使用 WebClient 呼叫 WCF
1 |
前陣子有讀者問起如何用 PowerShell Invoke-WebRequest 呼叫 WCF?我大概知道原理,只要組合出符合 WCF 要求的 XML,直接用 POSTMan/WebClient/HttpClient/Invoke-WebRequest 呼叫 WCF 不是難事,但囉嗦的 SOAP XML 有礙健康,既然用不到就別深究了。
造化弄人,手邊專案眼看也要整合 WCF,由於只用到一個 WCF 方法,不是很想為此加入參照在專案生出一堆自動產生程式碼跟噁心 XML 設定,給了我挑戰用 WebClient 搞定 WCF 的動機。另一方面,掌握此技巧也有助於養成徒手測試 WCF 能力,一舉兩得還算值得,便趁連假自主訓練健體強身。
首先要弄個 WCF 服務來測試驗證,這才發現 Visual Studio 2019 預設不支援 WCF 需另外安裝(參考: Installing WCF In Visual Studio 2019 ),基於某種潔癖、偏見或心靈創傷之類的不明心理因素,我不想平日寫 MVC、WebAPI 及 .NET Core 的開發環境被玷污,沒在 VS2019 安裝 WCF 元件建立新專案,而是抓現成的 WCF 範例專案回來改。在 CodeProject 找到一篇 Implementing a Basic Hello World WCF Service,打算用它當基礎修改。
HelloWorldService 範例原本只有一個 String GetMessage(String name) 方法,我想測試傳入 enum 跟結果傳回自訂型別以涵蓋常見情境,於是修改 IHelloWorldService.cs 及 HelloWorldService.cs 如下。
IHelloWorldService.cs 加入 List<Product> QueryProductsByCatetory(Categories catg) 並宣告 public enum Categories 及 public class Product (正規做法此二者應自成獨立 .cs 檔,示範用途為求單純化就塞在 IHelloWorldService.cs 吧):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Runtime.Serialization;
namespace MyWCFServices
{
[ServiceContract]
interface IHelloWorldService
{
[OperationContract]
String GetMessage(String name);
[OperationContract]
List<Product> QueryProductsByCatetory(Categories catg);
}
public enum Categories
{
Unknown,
Keyboard,
Mouse,
Set,
Part
}
[DataContract]
public class Product
{
[DataMember]
public string ModelId { get; set; }
[DataMember]
public Categories Category { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public decimal Price { get; set; }
[DataMember]
public int StockQty { get; set; }
public Product(string modelId, Categories catg, string name, decimal prz, int stkQty)
{
ModelId = modelId;
Category = catg;
Name = name;
Price = prz;
StockQty = stkQty;
}
}
}
HelloWorldService.cs 加上簡單的實作:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyWCFServices
{
public class HelloWorldService : IHelloWorldService
{
public String GetMessage(String name)
{
return "Hello world from " + name + "!";
}
static List<Product> stock = new List<Product>()
{
new Product("K100", Categories.Keyboard, "經典茶軸鍵盤", 1200, 100),
new Product("K250", Categories.Keyboard, "電競七彩背光鍵盤", 2500, 200),
new Product("M30", Categories.Mouse, "藍牙簡報滑鼠", 990, 150)
};
public List<Product> QueryProductsByCatetory(Categories catg)
{
return stock.Where(o => o.Category == catg).ToList();
}
}
}
用標準「Add Service Reference」做法寫了 Client 程式,測試無誤:
開啟 Fiddler 想觀察 XML 傳輸內容,我有點傻眼:
我之前寫 WCF 探勘時很習慣擷取封包觀察 XML,但這回抓到的訊息本體被加密了。
檢查範例專案 web.config 發現端倪:
<services>
<service name="MyWCFServices.HelloWorldService" behaviorConfiguration="MyServiceTypeBehaviors">
<endpoint address="" binding="wsHttpBinding" contract="MyWCFServices.IHelloWorldService"/>
<endpoint contract="IMetadataExchange" binding="mexHttpBinding" address="mex"/>
</service>
</services>
依據之前的整理 - WCF 預設 Binding 介紹,我理解到:之前玩 WCF 測試我幾乎都是用 BasicHttpBinding,預設加密模式為 None;而 CodeProject 的範例專案用的是 wsHttpBinding,預設為 Message 加密,但可以透過設定調整加上 <binding><security mode="None" /></binding> 停用加密(伺服器端跟客戶端都要調整)。
下圖為加密與否的傳輸形態比較。加密時一次 QueryProductsByCatetory() 會產生四次往返(黃底部分);設定 <security mode="None" /> 後只剩一次,傳輸量變小只需 961 Bytes:
停用加密後,從 Fiddler 取得一次呼叫的 Request 與 Response XML 內容如下:
Request
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">http://tempuri.org/IHelloWorldService/QueryProductsByCatetory</a:Action>
<a:MessageID>urn:uuid:47f2ab1a-be57-4ea6-a0d4-8477bd33f259</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">http://192.168.50.7/HelloWorld/HelloWorldService.svc</a:To>
</s:Header>
<s:Body>
<QueryProductsByCatetory xmlns="http://tempuri.org/">
<catg>Keyboard</catg>
</QueryProductsByCatetory>
</s:Body>
</s:Envelope>
Response
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">http://tempuri.org/IHelloWorldService/QueryProductsByCatetoryResponse</a:Action>
<a:RelatesTo>urn:uuid:47f2ab1a-be57-4ea6-a0d4-8477bd33f259</a:RelatesTo>
</s:Header>
<s:Body>
<QueryProductsByCatetoryResponse xmlns="http://tempuri.org/">
<QueryProductsByCatetoryResult xmlns:b="http://schemas.datacontract.org/2004/07/MyWCFServices" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<b:Product>
<b:Category>Keyboard</b:Category>
<b:ModelId>K100</b:ModelId>
<b:Name>經典茶軸鍵盤</b:Name>
<b:Price>1200</b:Price>
<b:StockQty>100</b:StockQty>
</b:Product>
<b:Product>
<b:Category>Keyboard</b:Category>
<b:ModelId>K250</b:ModelId>
<b:Name>電競七彩背光鍵盤</b:Name>
<b:Price>2500</b:Price>
<b:StockQty>200</b:StockQty>
</b:Product>
</QueryProductsByCatetoryResult>
</QueryProductsByCatetoryResponse>
</s:Body>
</s:Envelope>
掌握了 XML 格式,要改用 WebClient 或 PowerShell Invoke-WebRequest 直接呼叫 WCF 方法便不再是難事。以下為 C# 範例:
static void Main(string[] args)
{
var xdReq = XDocument.Parse(@"<s:Envelope xmlns:s=""http://www.w3.org/2003/05/soap-envelope""
xmlns:a=""http://www.w3.org/2005/08/addressing"">
<s:Header>
<a:Action s:mustUnderstand=""1"">http://tempuri.org/IHelloWorldService/QueryProductsByCatetory</a:Action>
<a:MessageID>urn:uuid:47f2ab1a-be57-4ea6-a0d4-8477bd33f259</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand=""1"">http://192.168.50.7/HelloWorld/HelloWorldService.svc</a:To>
</s:Header>
<s:Body>
<QueryProductsByCatetory xmlns=""http://tempuri.org/"">
<catg>Keyboard</catg>
</QueryProductsByCatetory>
</s:Body>
</s:Envelope>");
XNamespace ns = "http://tempuri.org/";
XNamespace ns_s = "http://www.w3.org/2003/05/soap-envelope";
XNamespace ns_a = "http://www.w3.org/2005/08/addressing";
var url = "http://192.168.50.7/HelloWorld/HelloWorldService.svc";
xdReq.Descendants(ns_a + "To").First().Value = url;
xdReq.Descendants(ns + "QueryProductsByCatetory")
.First().Element(ns + "catg").Value = "Mouse";
var wc = new WebClient();
wc.Encoding = Encoding.UTF8;
wc.Headers.Add(HttpRequestHeader.ContentType, "application/soap+xml; charset=utf-8");
var resp = wc.UploadString(url, xdReq.ToString());
var xdResp = XDocument.Parse(resp);
XNamespace ns_b = "http://schemas.datacontract.org/2004/07/MyWCFServices";
xdResp.Descendants(ns + "QueryProductsByCatetoryResult")
.First().Elements().ToList()
.ForEach(p =>
{
var propNames = "ModelId,Category,Name,Price,StockQty".Split(',');
foreach (var propName in propNames) {
Console.WriteLine($"{propName}: {p.Element(ns_b + propName).Value}");
}
Console.WriteLine("====");
});
}
執行結果:
程式並無深奧之處,純粹考驗對 XDocument 物件操作的熟練度,並需要懂 XML Namespace 是怎麼一回事。透過這種做法,就能做到不事先 Add Service Reference,動態串接 WCF 取得結果。但因為涉及 XML 繁瑣操作程式碼精簡不到哪裡去,加上只適用 None 或 Transport (HTTPS) 加密情境(手工處理 Message 加解密的複雜度太吃力,不划算),跟標準做法相比有沒有比較簡單,就見仁見智囉。
Tips of how to use pure WebClient to call WCF method without add service reference in design time.
Comments
# by Ike
我後來用了 VS 裡的 WcfTestClient 工具,來取得 POST 用的 XML,不用擷取封包真是太好了