筆記目錄

Skip to content

使用 HttpClient 呼叫 WebService

TLDR

  • 在無法加入 Web 參考的環境下,可透過 HttpClient 以 SOAP 訊息格式手動呼叫 WebService。
  • 針對 .NET Core 或無法使用 System.Web.Services 的環境,建議使用 XmlSerializer 進行物件與 XML 的序列化與反序列化,以處理複雜型別。
  • 透過 HttpClient.PostAsync 發送符合 SOAP 1.2 規範的 XML 內容,並解析回傳的 XDocument,即可實現動態呼叫。

解決無法加入 Web 參考的問題

什麼情況下會遇到這個問題:當開發環境因網路限制或架構限制,無法直接透過 Visual Studio 加入 Web 參考(WSDL)時。

在 .NET Framework 環境中,傳統做法是利用 WebClient 讀取 WSDL,並透過 ServiceDescriptionImporterReflection 動態編譯產生服務程式碼。但在 .NET Core 之後,由於缺乏 System.Web.Services 函式庫,此方法不再適用。此時,最穩定的替代方案是將 WebService 視為一個 HTTP 端點,直接透過 HttpClient 發送符合 SOAP 規範的 XML 請求。

使用 HttpClient 實作 SOAP 呼叫

什麼情況下會遇到這個問題:當需要呼叫 WebService,但無法使用自動產生的 Proxy 類別,且傳輸資料包含複雜物件時。

為了處理複雜的物件型別,我們可以使用 XmlSerializer 將 C# 物件轉換為 XML 格式,並封裝在 SOAP Envelope 中。以下是實作工具類別的建議做法:

csharp
public static class WebServiceUtils {
    private static readonly HttpClient httpClient = new HttpClient();

    public static async Task<TResponse> ExecuteAsync<TResponse>(string uri, string method, IDictionary<string, string> 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);
                // 使用 Regex 確保 XML 標籤名稱符合 WebService 預期的參數名稱
                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>
        ";

        StringContent content = new StringContent(soapXml, Encoding.UTF8, "text/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);
            }
        }
    }
}

驗證結果

透過上述方法,即使是包含巢狀結構的 RequestResponse 物件,也能正確地被序列化並傳遞給 WebService。在監看式中可以確認:

  • 請求參數已正確對應至 WebService 方法的參數名稱。
  • 回傳的 XML 已成功反序列化為指定的 TResponse 型別。

![webservice received request](../images/使用 HttpClient 呼叫 WebService/webservice-received-request.png) ![client received response](../images/使用 HttpClient 呼叫 WebService/client-received-response.png)

異動歷程

  • 2023-02-13 初版文件建立。