使用 HttpClient 呼叫 WebService
TLDR
- 在無法加入 Web 參考的環境下,可透過
HttpClient手動建構 SOAP 1.2 訊息來呼叫 WebService。 - 建議優先使用
dotnet-svcutil從 WSDL 產生強型別 Client,手刻 SOAP 僅適用於無法取得可用 WSDL 的極端情境。 - 使用
XmlSerializer處理複雜型別的序列化與反序列化,可確保參數傳遞與回傳值解析的正確性。 - 應避免頻繁建立
HttpClient實例,建議使用IHttpClientFactory或設定PooledConnectionLifetime以管理連線生命週期。 - SOAP 1.2 規範的 Media Type 應為
application/soap+xml。
為什麼需要手刻 HttpClient 呼叫 WebService
在 .NET 開發中,通常使用 Visual Studio 的「加入 Web 參考」功能即可完成 WebService 呼叫。然而,若開發環境無法連線至 WebService 導致無法加入參考,或 WSDL 檔案遺失、解析失敗時,則需要採取替代方案。
建議優先採用的方式:dotnet-svcutil
在 .NET Core 之後,正規做法是使用 dotnet-svcutil(或 Visual Studio 的「WCF Web Service Reference」)。此工具僅需本地的 WSDL 檔案即可產生強型別 Client,不需直接連線至服務端點。
注意事項
產生的 Client 是該份 WSDL 的快照。若 WebService 介面有異動,必須重新取得 WSDL 並重新產生 Client。
使用 HttpClient 與 XmlSerializer 實作 SOAP 呼叫
當無法取得可用 WSDL 時,可透過 HttpClient 發送 SOAP 1.2 請求,並利用 XmlSerializer 處理複雜物件的轉換。
實作邏輯
此方法的核心在於將物件序列化為 XML,並嵌入 SOAP Envelope 中。
csharp
public static class WebServiceUtils {
// 建議使用 IHttpClientFactory 管理生命週期,避免 Socket 耗盡或 DNS 解析問題
private static readonly HttpClient httpClient = new HttpClient();
public static async Task<TResponse> ExecuteAsync<TResponse>(string uri, string method, IDictionary<string, object> arguments, string @namespace = "http://tempuri.org/") {
XmlSerializerNamespaces serializerNamespaces = new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty });
XmlWriterSettings settings = new XmlWriterSettings {
Indent = true,
OmitXmlDeclaration = true
};
string argsXml = string.Join("", arguments.Select(x => {
Type type = x.Value.GetType();
XmlSerializer _serializer = new XmlSerializer(type);
StringBuilder sb = new StringBuilder();
using (XmlWriter writer = XmlWriter.Create(sb, settings)) {
_serializer.Serialize(writer, x.Value, serializerNamespaces);
// 替換 Root 節點名稱為 Dictionary Key
return Regex.Replace(sb.ToString(), $@"((?<=^<)(\w*)(?=>))|(?<=</)\w*(?=>$)", x.Key);
}
}));
string soapXml = $@"
<soap12:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap12=""http://www.w3.org/2003/05/soap-envelope"">
<soap12:Body>
<{method} xmlns=""{@namespace}"">
{argsXml}
</{method}>
</soap12:Body>
</soap12:Envelope>
";
// SOAP 1.2 建議使用 application/soap+xml
StringContent content = new StringContent(soapXml, Encoding.UTF8, "application/soap+xml");
using (HttpResponseMessage message = await httpClient.PostAsync(uri, content).ConfigureAwait(false)) {
if (!message.IsSuccessStatusCode) {
throw new HttpRequestException($"HTTP request failed with status code {message.StatusCode}: {message.ReasonPhrase}");
}
string result = await message.Content.ReadAsStringAsync().ConfigureAwait(false);
XDocument xdoc = XDocument.Parse(result);
XNamespace ns = @namespace;
string resultTag = method + "Result";
XElement xelement = xdoc.Descendants(ns + resultTag).Single();
XmlSerializer serializer = new XmlSerializer(typeof(TResponse), new XmlRootAttribute(resultTag) { Namespace = @namespace });
using (XmlReader reader = xelement.CreateReader()) {
return (TResponse)serializer.Deserialize(reader);
}
}
}
}驗證結果
透過定義巢狀的 Request 與 Response 類別進行測試,該實作能正確處理複雜型別的參數傳遞與回傳值解析。


異動歷程
- 初版文件建立。
- 修正
ExecuteAsync參數型別筆誤(IDictionary<string, string>應為IDictionary<string, object>)。 - 改寫 description 為實際的 HttpClient + SOAP 做法。
- 補上對應的可執行範例連結。
- 補充 HttpClient 生命週期與 SOAP 1.2 media type 的程式碼註解。
- 補上 svcutil 從 WSDL 產生 client 的 tip,並標明手刻做法的適用情境。
- 修正