☰
C#调用WebService的4种实战方案:从Add Web引用到动态编译
2026/10/11 19:52:20 网站建设 项目流程

简介:本资源是一份面向C#开发者与.NET技术学习者的实战教程,聚焦于现代环境下调用Web Service接口的核心方法与典型问题解决方案。针对分布式系统集成、跨语言服务调用等实际场景,系统讲解SOAP协议、WSDL解析、UDDI注册机制等底层原理,并对比分析“添加服务引用”自动生成代理类与手动构造SOAP请求两种主流方式,特别覆盖Java等异构平台提供的WebService兼容性难题、HTTP头认证(如WS-Security)、500错误排错思路等高频痛点。资源为单文件PDF文档(275KB),内容图文并茂,含完整可运行代码示例、信封结构详解及Header安全配置实操片段,便于快速查阅与本地验证。目前已有1548人学习下载,适合中高级C#工程师在项目对接、遗留系统集成或技术方案选型时参考使用。

1. C#调用WebService接口:不是“加个引用就完事”,而是三类场景下的四套落地方案

你有没有遇到过这种玄学时刻:SOAP UI点一下就返回200,C#代码跑起来却死在500 Internal Server Error?WSDL地址明明能浏览器打开,VS里“添加服务引用”却报错“无法下载元数据”?更扎心的是——对方Java写的WebService加了WS-Security头,你照着文档填了用户名密码,Reference.cs改了三遍,还是提示Security header not found?这不是你代码写得差,是传统Web引用机制在现代WebService生态里已经成了“半残废”。它只适配标准、静态、无安全增强的ASMX老古董;而真实产线中90%的WebService(尤其政务、金融、ERP对接)都带WS-Addressing、WS-Security、自定义Header、非标准命名空间,甚至WSDL本身是动态生成的。本文不讲SOAP协议原理,不堆WSDL语法树,只拆解4种可立即复现、带完整参数说明、覆盖主流坑点的调用路径:① .NET Framework下Add Web Reference的强化改造;② .NET Core/.NET 5+中用dotnet-svcutil生成强类型客户端;③ 手动构造SOAP信封+HttpClient的零依赖方案;④ 动态加载WSDL+编译代理类的“运行时反射”模式。适合两类人:一是正在被甲方WebService接口卡住进度的.NET开发,二是需要给遗留系统做兼容性封装的架构同学。所有代码均经实测(.NET 6 + Java Axis2 1.7.9 + Spring-WS 3.0.10),附带每种方案的适用边界和血泪排查清单。


2. Add Web Reference的强化改造:从“一键引用”到“手动续命”的全流程

Visual Studio的“添加Web引用”(Add Web Reference)功能在.NET Framework项目中仍是最快上手的方式,但它默认生成的代理类对现代WebService的兼容性极差。问题根源在于:它硬编码了SOAP 1.1协议、忽略WS-Security头、无法注入自定义HTTP Header、且生成的Reference.cs一旦WSDL变更就必须重新生成。但直接弃用它又太可惜——毕竟它自动生成了强类型方法签名、异常映射、异步包装。所以正确姿势是:先用它生成骨架,再手动注入关键能力。下面以一个带WS-Security认证的Java Axis2服务为例,完整走一遍改造流程。

2.1 创建基础引用并定位生成文件

在VS 2019(.NET Framework 4.7.2)中右键项目 → “添加Web引用” → 输入WSDL地址(如http://192.168.1.100:8080/axis2/services/UserService?wsdl)→ 命名空间填UserServiceRef→ 点击“添加引用”。VS会自动生成Reference.cs文件(位于项目名\Web References\UserServiceRef\Reference.cs)。注意:此时不要运行!因为Axis2默认要求WS-Security头,而Reference.cs里没有任何安全相关代码。

提示:若WSDL地址无法访问,请确认开发机网络策略是否允许访问该内网地址;若提示“无法下载元数据”,先用浏览器打开WSDL URL,检查是否返回XML内容(而非HTML登录页或404)。

2.2 注入WS-Security Header的代理类改造

打开Reference.cs,找到继承自System.Web.Services.Protocols.SoapHttpClientProtocol的主类(如UserService),在其构造函数中添加SoapHttpClientProtocol的初始化逻辑,并重写GetWebRequest方法以注入安全头:

// 在 UserService 类内部添加以下代码 private string _username = "hjq"; private string _password = "123"; protected override WebRequest GetWebRequest(Uri uri) { var request = base.GetWebRequest(uri); // 强制使用 SOAP 1.2 协议(Axis2 默认要求) if (request is HttpWebRequest httpReq) { httpReq.ContentType = "application/soap+xml; charset=utf-8"; httpReq.Headers.Add("SOAPAction", "\"\""); } return request; } // 新增方法:为每个请求注入 WS-Security Header protected override void OnInitiateRequest(WebRequest request) { base.OnInitiateRequest(request); if (request is HttpWebRequest httpReq) { // 生成唯一Nonce和Created时间戳(防重放) string nonce = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())); string created = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); // 构造WS-Security Header XML片段 string securityHeader = $@" <wsse:Security xmlns:wsse='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' xmlns:wsu='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' soap:mustUnderstand='1'> <wsse:UsernameToken wsu:Id='UsernameToken-1'> <wsse:Username>{_username}</wsse:Username> <wsse:Password Type='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText'>{_password}</wsse:Password> <wsse:Nonce EncodingType='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary'>{nonce}</wsse:Nonce> <wsu:Created>{created}</wsu:Created> </wsse:UsernameToken> </wsse:Security>"; // 将Security Header注入到SOAP Envelope中(需修改SendRequest方法) // 此处仅做Header注入示意,实际需Hook到序列化前 } }

但上述代码无法直接生效——因为OnInitiateRequest是虚方法,但SoapHttpClientProtocol并未提供注入SOAP Body的钩子。真正有效的做法是重写GetWebRequest并手动拼接完整SOAP信封:

// 替换原 UserService 类中的同步调用方法(如 GetUser) public GetUserResponse GetUser(GetUserRequest request) { // 1. 构造带Security Header的完整SOAP信封 string soapEnvelope = $@"<soap:Envelope xmlns:soap='http://www.w3.org/2003/05/soap-envelope' xmlns:ns='http://service.example.com/'> <soap:Header>{GetWSSecurityHeader()}</soap:Header> <soap:Body> <ns:GetUser> <ns:userId>{request.userId}</ns:userId> </ns:GetUser> </soap:Body> </soap:Envelope>"; // 2. 发送POST请求(复用基类的URL) string url = this.Url; // 即WSDL地址去掉?wsdl部分,如 http://192.168.1.100:8080/axis2/services/UserService return SendSoapRequest<GetUserResponse>(url, soapEnvelope); } private string GetWSSecurityHeader() { string nonce = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())); string created = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); return $@"<wsse:Security xmlns:wsse='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' xmlns:wsu='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' soap:mustUnderstand='1'> <wsse:UsernameToken wsu:Id='UsernameToken-1'> <wsse:Username>{_username}</wsse:Username> <wsse:Password Type='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText'>{_password}</wsse:Password> <wsse:Nonce EncodingType='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary'>{nonce}</wsse:Nonce> <wsu:Created>{created}</wsu:Created> </wsse:UsernameToken> </wsse:Security>"; } private T SendSoapRequest<T>(string url, string soapXml) where T : class { var request = (HttpWebRequest)WebRequest.Create(url); request.Method = "POST"; request.ContentType = "application/soap+xml; charset=utf-8"; request.Headers.Add("SOAPAction", "\"\""); // Axis2要求空SOAPAction using (var stream = request.GetRequestStream()) { var bytes = Encoding.UTF8.GetBytes(soapXml); stream.Write(bytes, 0, bytes.Length); } using (var response = (HttpWebResponse)request.GetResponse()) { using (var reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8)) { string xmlResult = reader.ReadToEnd(); // 解析XML响应(此处简化,实际应反序列化为T) return DeserializeSoapResponse<T>(xmlResult); } } }

这段代码的关键在于:绕过Reference.cs的自动序列化,完全接管SOAP信封构造与发送。GetWSSecurityHeader()确保每次请求都带新鲜Nonce和时间戳,避免重放攻击;SendSoapRequest强制使用SOAP 1.2协议头;DeserializeSoapResponse需自行实现XML解析(可用XmlSerializer或LINQ to XML)。此方案保留了Reference.cs的类型定义(如GetUserRequest),但抛弃了其通信逻辑,属于“半自动”模式。

2.3 配置文件驱动的凭证管理

硬编码用户名密码是生产环境大忌。应将凭证外置到app.config中:

<!-- app.config --> <configuration> <appSettings> <add key="WebService.Username" value="hjq"/> <add key="WebService.Password" value="123"/> <add key="WebService.EndpointUrl" value="http://192.168.1.100:8080/axis2/services/UserService"/> </appSettings> </configuration>

在UserService构造函数中读取:

public UserService() { this.Url = ConfigurationManager.AppSettings["WebService.EndpointUrl"]; _username = ConfigurationManager.AppSettings["WebService.Username"]; _password = ConfigurationManager.AppSettings["WebService.Password"]; }

这样既满足安全审计要求,又便于不同环境(DEV/UAT/PROD)切换配置。


3. dotnet-svcutil:.NET Core/.NET 5+中生成现代化SOAP客户端的官方方案

当项目迁移到.NET Core 3.1+或.NET 5/6/7时,“添加Web引用”功能彻底消失,取而代之的是命令行工具dotnet-svcutil。它比旧版更强大:支持SOAP 1.2、可生成异步方法、能处理复杂WSDL(含import、include)、且生成的客户端基于HttpClient而非过时的WebClient。但它的坑在于:默认生成的代码不包含任何安全头注入逻辑,且对WS-Security零支持。必须配合自定义MessageInspector才能补全。

3.1 使用dotnet-svcutil生成基础客户端

首先安装工具(全局一次):

dotnet tool install --global dotnet-svcutil

然后在项目目录下执行(假设WSDL地址为https://api.example.com/UserService.svc?wsdl):

dotnet-svcutil https://api.example.com/UserService.svc?wsdl ^ --outputDir .\GeneratedCode ^ --namespace "MyApp.UserService" ^ --syncMethod ^ --targetFramework net6.0

该命令会在.\GeneratedCode目录生成Reference.cs(含UserServiceClient类)和UserServiceReference.cs(含数据契约)。关键点:

  • --syncMethod生成同步方法(避免async/await嵌套地狱);
  • --targetFramework必须与项目目标框架一致,否则编译失败;
  • 若WSDL需认证(如HTTP Basic),添加--httpCredentials username:password参数。

生成后,在.csproj中添加引用:

<ItemGroup> <Compile Include="GeneratedCode\*.cs" /> </ItemGroup>

此时可直接调用:

var client = new UserServiceClient(UserServiceClient.EndpointConfiguration.BasicHttpBinding_IUserService); var result = client.GetUser(new GetUserRequest { userId = "123" });

但——这依然会因缺少WS-Security头而失败。接下来必须注入安全能力。

3.2 通过IEndpointBehavior注入WS-Security头

dotnet-svcutil生成的客户端继承自System.ServiceModel.ClientBase<T>,支持WCF行为扩展。我们创建一个WSSecurityBehavior类,实现IEndpointBehavior接口:

// WSSecurityBehavior.cs public class WSSecurityBehavior : IEndpointBehavior { private readonly string _username; private readonly string _password; public WSSecurityBehavior(string username, string password) { _username = username; _password = password; } public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { // 无需操作 } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { clientRuntime.MessageInspectors.Add(new WSSecurityMessageInspector(_username, _password)); } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { // 服务端行为,客户端忽略 } public void Validate(ServiceEndpoint endpoint) { // 验证逻辑 } } // WSSecurityMessageInspector.cs public class WSSecurityMessageInspector : IClientMessageInspector { private readonly string _username; private readonly string _password; public WSSecurityMessageInspector(string username, string password) { _username = username; _password = password; } public void AfterReceiveReply(ref Message reply, object correlationState) { // 接收响应后处理(如日志) } public object BeforeSendRequest(ref Message request, IClientChannel channel) { // 在发送请求前,向SOAP Header注入WS-Security var securityHeader = CreateWSSecurityHeader(); var newHeaders = MessageHeader.CreateHeader("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", securityHeader); request.Headers.Add(newHeaders); return null; } private XmlElement CreateWSSecurityHeader() { var doc = new XmlDocument(); string nonce = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())); string created = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); string xml = $@"<wsse:Security xmlns:wsse='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd' xmlns:wsu='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' soap:mustUnderstand='1'> <wsse:UsernameToken wsu:Id='UsernameToken-1'> <wsse:Username>{_username}</wsse:Username> <wsse:Password Type='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText'>{_password}</wsse:Password> <wsse:Nonce EncodingType='http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary'>{nonce}</wsse:Nonce> <wsu:Created>{created}</wsu:Created> </wsse:UsernameToken> </wsse:Security>"; doc.LoadXml(xml); return doc.DocumentElement; } }

3.3 在客户端实例中应用行为

创建客户端时,显式添加行为:

var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport); // 或 TransportWithMessageCredential binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None; var endpoint = new EndpointAddress("https://api.example.com/UserService.svc"); var client = new UserServiceClient(binding, endpoint); // 应用安全行为 client.Endpoint.Behaviors.Add(new WSSecurityBehavior("hjq", "123")); // 调用 var result = client.GetUser(new GetUserRequest { userId = "123" });

此方案的优势在于:完全遵循WCF标准扩展机制,不侵入生成代码,且可复用于多个服务客户端。但注意:dotnet-svcutil生成的绑定默认是BasicHttpBinding,若服务要求CustomBinding(如启用MTOM),需手动修改生成的Binding配置。


4. 手动构造SOAP信封:零依赖、全掌控的HttpClient方案

当项目不允许引用System.ServiceModel(如纯.NET 6 Minimal API),或WSDL结构过于诡异(如动态WSDL、多命名空间嵌套),或需要极致性能控制时,手动构造SOAP信封+HttpClient是最可靠的选择。它不依赖任何框架生成代码,所有逻辑透明可控,且天然支持异步、超时、重试、日志埋点。

4.1 构建可复用的SOAP客户端基类

创建SoapHttpClient类,封装通用SOAP调用逻辑:

// SoapHttpClient.cs public class SoapHttpClient { private readonly HttpClient _httpClient; private readonly string _serviceUrl; private readonly string _soapAction; // 可选,部分服务需要 public SoapHttpClient(string serviceUrl, string soapAction = null) { _serviceUrl = serviceUrl; _soapAction = soapAction; _httpClient = new HttpClient(); // 设置默认超时(避免无限等待) _httpClient.Timeout = TimeSpan.FromSeconds(30); } public async Task<T> PostAsync<T>(string soapEnvelope, CancellationToken cancellationToken = default) where T : class { try { var content = new StringContent(soapEnvelope, Encoding.UTF8, "application/soap+xml; charset=utf-8"); // 添加SOAPAction头(若服务要求) if (!string.IsNullOrEmpty(_soapAction)) { content.Headers.Add("SOAPAction", $"\"{_soapAction}\""); } var response = await _httpClient.PostAsync(_serviceUrl, content, cancellationToken); response.EnsureSuccessStatusCode(); // 抛出非2xx异常 var responseXml = await response.Content.ReadAsStringAsync(cancellationToken); return DeserializeResponse<T>(responseXml); } catch (HttpRequestException ex) { throw new InvalidOperationException($"SOAP调用失败: {_serviceUrl}, 错误: {ex.Message}", ex); } } private T DeserializeResponse<T>(string xml) where T : class { // 使用XmlSerializer反序列化(需T有[XmlRoot]等特性) try { var serializer = new XmlSerializer(typeof(T)); using var reader = new StringReader(xml); return (T)serializer.Deserialize(reader); } catch (InvalidOperationException ex) { // 若反序列化失败,返回原始XML供调试 throw new InvalidOperationException($"响应XML解析失败:\n{xml.Substring(0, Math.Min(500, xml.Length))}...", ex); } } }

4.2 针对不同WebService的信封模板

不同服务对SOAP信封格式要求差异极大。以下是三种高频场景的模板:

场景WSDL来源SOAP版本关键特征模板要点
Java Axis2Axis2 1.7+SOAP 1.2要求空SOAPAction,WS-Security头必填<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">+<soap:Header>注入Security
.NET ASMX.NET FrameworkSOAP 1.1SOAPAction头必填,值为"http://tempuri.org/MethodName"<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
Spring-WSSpring BootSOAP 1.1/1.2支持wsa:Action头,常要求wsa:To地址需添加<wsa:Action xmlns:wsa="http://www.w3.org/2005/08/addressing">http://example.com/GetUser</wsa:Action>

以Axis2为例,构造完整调用:

var client = new SoapHttpClient("http://192.168.1.100:8080/axis2/services/UserService"); string soapEnvelope = $@"<soap:Envelope xmlns:soap='http://www.w3.org/2003/05/soap-envelope' xmlns:ns='http://service.example.com/'> <soap:Header>{GetWSSecurityHeader("hjq", "123")}</soap:Header> <soap:Body> <ns:GetUser> <ns:userId>123</ns:userId> </ns:GetUser> </soap:Body> </soap:Envelope>"; var result = await client.PostAsync<GetUserResponse>(soapEnvelope);

其中GetWSSecurityHeader复用前文逻辑。此方案最大优势是完全解耦:不依赖WSDL解析、不生成任何代理类、不绑定特定.NET版本,一个SoapHttpClient实例可调用任意SOAP服务。

4.3 错误响应的精细化处理

SOAP错误不走HTTP状态码,而是返回<soap:Fault>节点。需在PostAsync中增强错误解析:

private async Task<T> ParseSoapResponse<T>(HttpResponseMessage response, string responseXml, CancellationToken cancellationToken) where T : class { // 检查是否为SOAP Fault if (responseXml.Contains("<soap:Fault>") || responseXml.Contains("<faultstring>")) { var fault = ExtractFaultDetails(responseXml); throw new SoapFaultException(fault.Code, fault.Reason, fault.Detail); } return DeserializeResponse<T>(responseXml); } private (string Code, string Reason, string Detail) ExtractFaultDetails(string xml) { var doc = XDocument.Parse(xml); var ns = XNamespace.Get("http://www.w3.org/2003/05/soap-envelope"); var fault = doc.Descendants(ns + "Fault").FirstOrDefault(); if (fault == null) return ("Unknown", "Invalid SOAP Fault", xml); var code = fault.Element(ns + "Code")?.Element(ns + "Value")?.Value ?? "Unknown"; var reason = fault.Element(ns + "Reason")?.Element(ns + "Text")?.Value ?? "No reason"; var detail = fault.Element(ns + "Detail")?.ToString() ?? "No detail"; return (code, reason, detail); }

这样当服务返回<soap:Fault><soap:Code><soap:Value>wsse:FailedAuthentication</soap:Value></soap:Code>时,能精准捕获并抛出SoapFaultException,避免业务层混淆HTTP错误与SOAP业务错误。


5. 动态WSDL加载与运行时编译:应对WSDL不可达或频繁变更的终极方案

某些场景下,WSDL地址根本无法从开发机访问(如甲方内网隔离),或WSDL每日动态生成(如测试环境Mock服务),此时静态生成代理类完全失效。解决方案是:运行时下载WSDL → 解析为ServiceDescription → 动态编译为Assembly → 反射调用。这是最“黑匣子”但也最灵活的方案,本质是VS“添加Web引用”的底层逻辑复现。

5.1 下载并解析WSDL的健壮实现

WSDL可能被防火墙拦截、需HTTP Basic认证、或返回压缩内容。DownloadWSDL方法需处理这些:

private static async Task<ServiceDescription> DownloadWSDLAsync(string wsdlUrl, string username = null, string password = null, CancellationToken cancellationToken = default) { var handler = new HttpClientHandler(); if (!string.IsNullOrEmpty(username)) { handler.Credentials = new NetworkCredential(username, password); } using var client = new HttpClient(handler); client.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip,deflate"); try { var response = await client.GetAsync(wsdlUrl, cancellationToken); response.EnsureSuccessStatusCode(); var wsdlContent = await response.Content.ReadAsStringAsync(cancellationToken); // 处理WSDL中import/include的相对路径(关键!) wsdlContent = ResolveWsdlImports(wsdlContent, wsdlUrl); using var reader = new StringReader(wsdlContent); return ServiceDescription.Read(reader); } catch (HttpRequestException ex) { throw new InvalidOperationException($"无法下载WSDL: {wsdlUrl}, 错误: {ex.Message}", ex); } } private static string ResolveWsdlImports(string wsdlContent, string baseUrl) { // 使用正则替换 <import location="xxx.wsdl"/> 为绝对路径 var regex = new Regex(@"<import\s+.*?location=""(.*?)""", RegexOptions.IgnoreCase); return regex.Replace(wsdlContent, match => { var relativePath = match.Groups[1].Value; var absoluteUrl = new Uri(new Uri(baseUrl), relativePath).ToString(); return $"<import location=\"{absoluteUrl}\""; }); }

5.2 动态编译服务代理类

核心逻辑:用ServiceDescriptionImporter将WSDL转为CodeDom,再用CSharpCodeProvider编译为内存Assembly:

private static Assembly CompileServiceAssembly(ServiceDescription sd, string serviceName = null) { var importer = new ServiceDescriptionImporter(); importer.ProtocolName = "Soap"; // 或 "Soap12" importer.Style = ServiceDescriptionImportStyle.Client; importer.CodeGenerationOptions = CodeGenerationOptions.GenerateProperties; // 添加WSDL到importer importer.AddServiceDescription(sd, "", ""); var nspace = new CodeNamespace("DynamicWebService"); var unit = new CodeCompileUnit(); unit.Namespaces.Add(nspace); ServiceDescriptionImportWarnings warnings; try { warnings = importer.Import(nspace, unit); } catch (Exception ex) { throw new InvalidOperationException("WSDL解析失败,请检查WSDL格式是否有效", ex); } if (warnings != 0) { throw new InvalidOperationException($"WSDL导入警告: {warnings}"); } // 编译参数 var provider = new CSharpCodeProvider(); var parameters = new CompilerParameters { GenerateInMemory = true, GenerateExecutable = false, IncludeDebugInformation = false }; // 添加必要引用(.NET Core需特别注意) var assemblies = new[] { "System.dll", "System.Xml.dll", "System.Web.Services.dll", "System.Data.dll" }; parameters.ReferencedAssemblies.AddRange(assemblies); var results = provider.CompileAssemblyFromDom(parameters, unit); if (results.Errors.HasErrors) { var error = string.Join(";", results.Errors.Cast<CompilerError>().Select(e => e.ErrorText)); throw new InvalidOperationException($"编译失败: {error}"); } return results.CompiledAssembly; }

5.3 运行时反射调用服务方法

编译完成后,通过反射获取服务类型并调用:

public static async Task<object> InvokeWebServiceAsync( string wsdlUrl, string methodName, object[] parameters, string username = null, string password = null, CancellationToken cancellationToken = default) { // 1. 下载WSDL var sd = await DownloadWSDLAsync(wsdlUrl, username, password, cancellationToken); // 2. 动态编译 var assembly = CompileServiceAssembly(sd); // 3. 查找服务类型(通常为WSDL中<service>的name属性) var serviceName = sd.Services.Count > 0 ? sd.Services[0].Name : "Service"; var serviceType = assembly.GetType($"DynamicWebService.{serviceName}"); if (serviceType == null) { throw new InvalidOperationException($"未找到服务类型: {serviceName}"); } // 4. 创建实例并设置凭证(关键!) var instance = Activator.CreateInstance(serviceType); if (username != null && serviceType.GetProperty("Credentials") != null) { var credentials = new NetworkCredential(username, password); serviceType.GetProperty("Credentials").SetValue(instance, credentials); } // 5. 查找并调用方法 var method = serviceType.GetMethod(methodName); if (method == null) { throw new InvalidOperationException($"服务中未找到方法: {methodName}"); } try { return method.Invoke(instance, parameters); } catch (TargetInvocationException ex) { throw ex.InnerException ?? ex; } } // 使用示例 var result = await InvokeWebServiceAsync( "http://192.168.1.100:8080/axis2/services/UserService?wsdl", "GetUser", new object[] { "123" }, "hjq", "123" );

此方案虽灵活,但性能开销大(每次调用都编译)、调试困难、且.NET Core中ServiceDescriptionImporter已标记为[Obsolete]。仅推荐用于WSDL绝对不可达、且调用频次极低的管理后台场景。


6. 避坑指南:C#调用WebService的5个血泪经验与排查清单

无论你选择哪种方案,以下5个坑点几乎必然出现。它们不是理论缺陷,而是真实产线中反复翻车的“后悔药”清单。每一条都来自某开发者在凌晨三点对着Fiddler抓包的绝望截图。

6.1 现象:SOAP UI能通,C#代码500错误,Fiddler显示<soap:Fault><faultcode>soap:VersionMismatch</faultcode>

原因:SOAP UI默认发送SOAP 1.1信封,而服务端(如Axis2)配置为仅接受SOAP 1.2,但C#生成的客户端未显式指定协议版本。
解决:

  • 对于Add Web Reference:在Reference.cs中修改SoapHttpClientProtocol的SoapVersion属性为SoapProtocolVersion.Soap12;
  • 对于dotnet-svcutil:生成时添加--binding-configuration "BasicHttpBinding_IService"并在appsettings.json中配置<basicHttpBinding><binding messageEncoding="Mtom" /></basicHttpBinding>;
  • 对于手动HttpClient:信封根节点必须用xmlns:soap="http://www.w3.org/2003/05/soap-envelope"(SOAP 1.2),而非http://schemas.xmlsoap.org/soap/envelope/(SOAP 1.1)。

6.2 现象:WSDL能浏览器打开,VS“添加Web引用”报错“无法下载元数据”,日志显示The remote server returned an error: (401) Unauthorized

原因:WSDL地址需要HTTP Basic认证,但VS的Web引用向导不支持凭据传递。
解决:

  • 先用浏览器登录WSDL地址(输入账号密码),让Cookie生效;
  • 或在app.config中配置<system.net><defaultProxy><proxy usesystemdefault="true"/></defaultProxy></system.net>,让VS走系统代理;
  • 最佳实践:用curl -u user:pass -o service.wsdl http://url?wsdl下载WSDL文件,再在VS中“添加Web引用”时选择本地service.wsdl文件。

6.3 现象:调用返回<soap:Fault><faultcode>wsse:FailedAuthentication</faultcode>,但用户名密码确认无误

原因:WS-Security头中的<wsu:Created>时间戳与服务端时间偏差超过5分钟(常见于虚拟机时钟漂移),或<wsse:Nonce>重复使用(未每次生成新值)。
解决:

  • 服务端时间必须与NTP服务器同步(误差<1分钟);
  • 客户端GetWSSecurityHeader()中nonce必须用Guid.NewGuid()生成,created必须用DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
  • 若服务端要求<wsu:Expires>,需添加<wsu:Expires>{DateTime.UtcNow.AddMinutes(5).ToString("yyyy-MM-ddTHH:mm:ss.fffZ")}</wsu:Expires>。

6.4 现象:dotnet-svcutil生成的客户端调用时报错Could not find default endpoint element that references contract 'IService'

原因:生成的Reference.cs中UserServiceClient类的构造函数试图从app.config读取endpoint配置,但.NET Core项目没有app.config。
解决:

  • 不要使用无参构造函数,改为显式传入Binding和EndpointAddress:
    var binding = new BasicHttpBinding(BasicHttpSecurityMode.None); var endpoint = new EndpointAddress("https://api.example.com/Service.svc"); var client = new UserServiceClient(binding, endpoint);
  • 或在.NET 6+中,将endpoint配置写入Program.cs:
    builder.Services.AddSingleton(s => new UserServiceClient( new BasicHttpBinding(), new EndpointAddress("https://api.example.com/Service.svc")));

6.5 现象:手动构造SOAP信封调用成功,但返回的XML中中文乱码(显示为??)

原因:StringContent未指定UTF-8 BOM,且服务端未正确声明encoding="utf-8"。
解决:

  • 强制在SOAP信封开头添加XML声明:

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询