首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

C# 動態呼叫 Web Service (WSDL) 完整實作:免除 Proxy 類別依賴

     編輯:ASP.NET 基礎

應用場景

  • Web Service 端點頻繁變更,不想每次重新產生 Proxy 類別。
  • 執行時才知道 WSDL URL (如多租戶、動態發現)。
  • 輕量級專案,不想引入 WCF/Service Reference 產生的大量程式碼。

核心原理

  1. 下載 WSDL (WebClient.OpenRead(url + "?WSDL"))。
  2. ServiceDescriptionImporter 產生客戶端 Proxy 原始碼 (CodeDOM)。
  3. CSharpCodeProvider 記憶體編譯組件 (CompilerParameters.GenerateInMemory = true)。
  4. 反射建立執行個體並呼叫目標方法 (MethodInfo.Invoke)。

通用輔助類別


using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Web.Services.Description;
using System.Net;
using System.IO;
using Microsoft.CSharp;

public static class DynamicWsClient
{
    private const string DefaultNamespace = "DynamicWsProxy";

    public static object Invoke(string wsdlUrl, string methodName, params object[] args)
    {
        return Invoke(wsdlUrl, null, methodName, args);
    }

    public static object Invoke(string wsdlUrl, string className, string methodName, params object[] args)
    {
        // 1. 下載 WSDL
        using (var wc = new WebClient())
        using (var stream = wc.OpenRead(wsdlUrl + "?WSDL"))
        {
            var sd = ServiceDescription.Read(stream);
            var importer = new ServiceDescriptionImporter();
            importer.AddServiceDescription(sd, "", "");
            importer.ProtocolName = "Soap12"; // 或 Soap
            importer.Style = ServiceDescriptionImportStyle.Client;
            importer.CodeGenerationOptions = CodeGenerationOptions.GenerateProperties | CodeGenerationOptions.GenerateNewAsync;

            // 2. 產生 CodeDOM
            var ns = new CodeNamespace(DefaultNamespace);
            var unit = new CodeCompileUnit();
            unit.Namespaces.Add(ns);
            var errors = importer.Import(ns, unit);
            if (errors > 0) throw new InvalidOperationException("WSDL 匯入失敗");

            // 3. 記憶體編譯
            var provider = new CSharpCodeProvider();
            var cp = new CompilerParameters
            {
                GenerateInMemory = true,
                GenerateExecutable = false,
                TreatWarningsAsErrors = false
            };
            cp.ReferencedAssemblies.Add("System.dll");
            cp.ReferencedAssemblies.Add("System.Xml.dll");
            cp.ReferencedAssemblies.Add("System.Web.Services.dll");
            cp.ReferencedAssemblies.Add("System.Data.dll");

            var results = provider.CompileAssemblyFromDom(cp, unit);
            if (results.Errors.HasErrors)
            {
                var msg = string.Join(Environment.NewLine, results.Errors.Cast().Select(e => e.ToString()));
                throw new InvalidOperationException("Proxy 編譯失敗:\n" + msg);
            }

            // 4. 反射呼叫
            var asm = results.CompiledAssembly;
            var typeName = className ?? ExtractClassName(wsdlUrl);
            var type = asm.GetType(DefaultNamespace + "." + typeName, true, true);
            var instance = Activator.CreateInstance(type);
            var method = type.GetMethod(methodName);
            if (method == null) throw new MissingMethodException($"方法 {methodName} 不存在");
            return method.Invoke(instance, args);
        }
    }

    private static string ExtractClassName(string url)
    {
        // 從 URL 取得服務名稱,如 .../MyService.asmx → MyService
        var uri = new Uri(url);
        var segments = uri.Segments;
        var file = segments[segments.Length - 1];
        return Path.GetFileNameWithoutExtension(file);
    }
}

使用範例


// 呼叫公開的火車時刻表 Web Service
string wsdl = "https://webservice.webxml.com.cn/WebServices/TrainTimeWebService.asmx";
object result = DynamicWsClient.Invoke(wsdl, "getDetailInfoByTrainCode", "K123", "");

if (result is System.Data.DataSet ds)
{
    // 繫結到 GridView
    GridView1.DataSource = ds.Tables[0];
    GridView1.DataBind();
}

進階:支援非同步與複雜型別


// 非同步版本 (APM 模式)
public static IAsyncResult BeginInvoke(string wsdlUrl, string methodName, object[] args, AsyncCallback callback, object state)
{
    // 同步產生 Proxy (可快取組件提升效能)
    var asm = BuildProxyAssembly(wsdlUrl);
    var type = asm.GetType(DefaultNamespace + "." + ExtractClassName(wsdlUrl));
    var instance = Activator.CreateInstance(type);
    var beginMethod = type.GetMethod("Begin" + methodName);
    return (IAsyncResult)beginMethod.Invoke(instance, args.Concat(new object[] { callback, state }).ToArray());
}

public static object EndInvoke(string wsdlUrl, string methodName, IAsyncResult asyncResult)
{
    var asm = BuildProxyAssembly(wsdlUrl); // 應快取
    var type = asm.GetType(DefaultNamespace + "." + ExtractClassName(wsdlUrl));
    var instance = Activator.CreateInstance(type);
    var endMethod = type.GetMethod("End" + methodName);
    return endMethod.Invoke(instance, new object[] { asyncResult });
}

// 組件快取策略 (簡易版)
private static readonly ConcurrentDictionary _asmCache = new ConcurrentDictionary();

private static Assembly BuildProxyAssembly(string wsdlUrl)
{
    return _asmCache.GetOrAdd(wsdlUrl, url => {
        // ... 前面的 WSDL 匯入與編譯邏輯 ...
        return results.CompiledAssembly;
    });
}

效能與快取策略

  • 組件快取必要:每次呼叫動態編譯極慢 (數百毫秒)。生產環境必須快取 Assembly 物件,Key 為 WSDL URL。
  • WSDL 變更偵測:可定期下載 WSDL 計算雜湊值比對,變更時清除快取重建。
  • 並發安全:使用 ConcurrentDictionary.GetOrAdd 確保單次建構。

現代替代方案建議

方案優點缺點
HttpClient + 手工 SOAP XML零依賴、完全控制、效能最佳需手寫 XML、處理命名空間、SOAP Header
WCF ChannelFactory<T>型別安全、支援合約介面、內建快取需共用合約組件、配置較繁瑣
Service Reference (VS 產生 Proxy)開發體驗最好、IntelliSense、設計時驗證端點變更需重新產生、程式碼膨脹
動態編譯 (本文方案)執行時決定端點、無設計時依賴首次冷啟動慢、除錯困難、錯誤訊息不友善

**新專案首選 HttpClient + 手工 SOAP** 或 **gRPC/REST API** 取代傳統 ASMX/WCF。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved