在企業級應用開發中,WebService 呼叫通常透過 Visual Studio「加入服務參考」產生靜態 Proxy 類別。但當服務端點頻繁變動、或需在執行期動態決定呼叫哪個服務時,靜態參考顯得不夠彈性。本文介紹如何使用 HttpWebRequest 與 ServiceDescriptionImporter 實現完全動態的 WebService 呼叫,支援 SOAP、HTTP GET/POST 多種協定。
以下為核心類別 WebServiceCaller,封裝了三種呼叫方式:
public static XmlDocument QueryPostWebService(string url, string methodName, Hashtable pars)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url + "/" + methodName);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
SetRequest(request);
byte[] data = EncodePars(pars);
WriteRequestData(request, data);
return ReadXmlResponse(request.GetResponse());
}
public static XmlDocument QueryGetWebService(string url, string methodName, Hashtable pars)
{
string queryString = ParsToString(pars);
HttpWebRequest request = (HttpWebRequest)WebRequest.Create($"{url}/{methodName}?{queryString}");
request.Method = "GET";
request.ContentType = "application/x-www-form-urlencoded";
SetRequest(request);
return ReadXmlResponse(request.GetResponse());
}
public static XmlDocument QuerySoapWebService(string url, string methodName, Hashtable pars)
{
string xmlNs = _xmlNamespaces.ContainsKey(url)
? _xmlNamespaces[url].ToString()
: GetNamespace(url); // 快取命名空間
return QuerySoapWebService(url, methodName, pars, xmlNs);
}
private static XmlDocument QuerySoapWebService(string url, string methodName, Hashtable pars, string xmlNs)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.ContentType = "text/xml; charset=utf-8";
request.Headers.Add("SOAPAction", $"\"{xmlNs}{(xmlNs.EndsWith("/") ? "" : "/")}{methodName}\"");
SetRequest(request);
byte[] soapData = EncodeParsToSoap(pars, xmlNs, methodName);
WriteRequestData(request, soapData);
XmlDocument doc = ReadXmlResponse(request.GetResponse());
// 擷取 SOAP Body 實際回傳內容
XmlNamespaceManager mgr = new XmlNamespaceManager(doc.NameTable);
mgr.AddNamespace("soap", "https://schemas.xmlsoap.org/soap/envelope/");
string retXml = doc.SelectSingleNode("//soap:Body/*/*", mgr).InnerXml;
XmlDocument result = new XmlDocument();
result.LoadXml("<root>" + retXml + "</root>");
AddDeclaration(result);
return result;
}
GetNamespace(url):下載 WSDL 並解析 targetNamespace。EncodeParsToSoap():將 Hashtable 參數序列化為 SOAP XML 訊息。ObjectToSoapXml():使用 XmlSerializer 處理複雜型別參數。當 WebService 涉及複雜資料結構(如 DataSet、自訂類別),手工組裝 SOAP 訊息極易出錯。進階做法是執行期下載 WSDL,使用 CodeDom 動態編譯產生 Proxy DLL,再透過 Reflection 呼叫。
WebClient 下載 WSDL 內容。ServiceDescriptionImporter 產生客戶端 Proxy 程式碼。CodeDomProvider 編譯為記憶體或暫存檔 DLL。Assembly.LoadFrom 載入組件,Activator.CreateInstance 建立 Proxy 實例。MethodInfo.Invoke 動態呼叫目標方法。// 快取機制:依 WSDL 內容 MD5 命名 DLL,避免重複編譯
string assPath = Path.GetTempPath() + assName + GetMd5Sum(wsdlUrl) + ".dll";
if (File.Exists(assPath)) // 快取命中
{
InitTypeName();
return;
}
// 編譯參數設定
CompilerParameters cp = new CompilerParameters();
cp.ReferencedAssemblies.AddRange(new[] { "System.dll", "System.Xml.dll", "System.Web.Services.dll", "System.Data.dll" });
cp.GenerateInMemory = false; // 必須產生實體檔案才能 LoadFrom
cp.OutputAssembly = assPath;
CompilerResults cr = provider.CompileAssemblyFromDom(cp, unit);
if (cr.Errors.HasErrors) throw new Exception("動態編譯失敗:" + cr.Errors[0].ErrorText);
File.Copy(cr.PathToAssembly, assPath, true); // 複製到快取路徑
InitTypeName(); // 反射取得 Proxy 類別 Type
// 方案一:簡單 SOAP 呼叫
Hashtable ht = new Hashtable();
ht.Add("xmlIn", "<Request><MemCode>001</MemCode></Request>");
XmlDocument result = WebServiceCaller.QuerySoapWebService(
"https://example.com/Service.asmx", "SinPointQuery", ht);
// 方案二:強型別 Proxy 呼叫
WebServiceProxy proxy = new WebServiceProxy("https://localhost:8080/Service?wsdl", "MyService");
object[] args = new object[] { "參數1", "參數2" };
string ret = (string)proxy.ExecuteQuery("MethodName", args);
appSettings 或資料庫,上線時只需修改設定值。web.config 已啟用 <protocols> 中的 HttpGet、HttpPost、HttpSoap。動態 WebService 呼叫技術讓系統具備極高彈性,特別適合多租戶、微服務閘道、或第三方整合平台等情境。根據專案複雜度選擇輕量級 HttpWebRequest 或完整 Proxy 編譯方案,皆能有效解決靜態參考的維護痛點。