.NET 6通过服务引用调用WCF远程服务故障排查求助
我花了一周时间寻找解决方案,仍毫无头绪。需求是将一个远程托管的SOAP服务接入类库,供ASP.NET WebApi调用,但无论如何尝试都无法获取服务响应:常规情况返回null,取消服务实现类的DebuggerStepThroughAttribute并断点调试时,触发System.ServiceModel.CommunicationObjectFaultedException,推测客户端未发送正确请求。用Postman发送带XML体的POST请求可正常获取响应,说明远程服务无故障。
我已查阅各类文档及CoreWCF官方示例,仍未解决问题。代码均由svcutil生成,依赖注入和配置类为自行编写,相关文件如下:
VS2022生成的connectedServices.json文件:
{ "ExtendedData": { "inputs": [ "https://services.rs.ge/WayBillService/WayBillService.asmx" ], "collectionTypes": [ "System.Array", "System.Collections.Generic.Dictionary`2" ], "namespaceMappings": [ "*, RevenueService" ], "targetFramework": "net6.0", "typeReuseMode": "All" } }
生成的Channel接口:
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.0.3")] public interface WayBillsSoapChannel : RevenueService.WayBillsSoap, System.ServiceModel.IClientChannel { }
含方法的ServiceContract:
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.0.3")] [System.ServiceModel.ServiceContractAttribute(ConfigurationName="RevenueService.WayBillsSoap")] public interface WayBillsSoap { [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/get_error_codes", ReplyAction="*")] System.Threading.Tasks.Task<RevenueService.get_error_codesResponse> get_error_codesAsync(RevenueService.get_error_codesRequest request); }
服务契约实现类:
[System.Diagnostics.DebuggerStepThroughAttribute()] [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.0.3")] public partial class WayBillsSoapClient : System.ServiceModel.ClientBase<RevenueService.WayBillsSoap>, RevenueService.WayBillsSoap { /// <summary> /// Implement this partial method to configure the service endpoint. /// </summary> /// <param name="serviceEndpoint">The endpoint to configure</param> /// <param name="clientCredentials">The client credentials</param> static partial void ConfigureEndpoint(System.ServiceModel.Description.ServiceEndpoint serviceEndpoint, System.ServiceModel.Description.ClientCredentials clientCredentials); public WayBillsSoapClient(EndpointConfiguration endpointConfiguration) : base(WayBillsSoapClient.GetBindingForEndpoint(endpointConfiguration), WayBillsSoapClient.GetEndpointAddress(endpointConfiguration)) { this.Endpoint.Name = endpointConfiguration.ToString(); ConfigureEndpoint(this.Endpoint, this.ClientCredentials); } public WayBillsSoapClient(EndpointConfiguration endpointConfiguration, string remoteAddress) : base(WayBillsSoapClient.GetBindingForEndpoint(endpointConfiguration), new System.ServiceModel.EndpointAddress(remoteAddress)) { this.Endpoint.Name = endpointConfiguration.ToString(); ConfigureEndpoint(this.Endpoint, this.ClientCredentials); } public WayBillsSoapClient(EndpointConfiguration endpointConfiguration, System.ServiceModel.EndpointAddress remoteAddress) : base(WayBillsSoapClient.GetBindingForEndpoint(endpointConfiguration), remoteAddress) { this.Endpoint.Name = endpointConfiguration.ToString(); ConfigureEndpoint(this.Endpoint, this.ClientCredentials); } public WayBillsSoapClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : base(binding, remoteAddress) { } public virtual System.Threading.Tasks.Task OpenAsync() { return System.Threading.Tasks.Task.Factory.FromAsync(((System.ServiceModel.ICommunicationObject)(this)).BeginOpen(null, null), new System.Action<System.IAsyncResult>(((System.ServiceModel.ICommunicationObject)(this)).EndOpen)); } public virtual System.Threading.Tasks.Task CloseAsync() { return System.Threading.Tasks.Task.Factory.FromAsync(((System.ServiceModel.ICommunicationObject)(this)).BeginClose(null, null), new System.Action<System.IAsyncResult>(((System.ServiceModel.ICommunicationObject)(this)).EndClose)); } private static System.ServiceModel.Channels.Binding GetBindingForEndpoint(EndpointConfiguration endpointConfiguration) { if ((endpointConfiguration == EndpointConfiguration.WayBillsSoap)) { System.ServiceModel.BasicHttpBinding result = new System.ServiceModel.BasicHttpBinding(); result.MaxBufferSize = int.MaxValue; result.ReaderQuotas = System.Xml.XmlDictionaryReaderQuotas.Max; result.MaxReceivedMessageSize = int.MaxValue; result.AllowCookies = true; result.Security.Mode = System.ServiceModel.BasicHttpSecurityMode.Transport; return result; } if ((endpointConfiguration == EndpointConfiguration.WayBillsSoap12)) { System.ServiceModel.Channels.CustomBinding result = new System.ServiceModel.Channels.CustomBinding(); System.ServiceModel.Channels.TextMessageEncodingBindingElement textBindingElement = new System.ServiceModel.Channels.TextMessageEncodingBindingElement(); textBindingElement.MessageVersion = System.ServiceModel.Channels.MessageVersion.CreateVersion(System.ServiceModel.EnvelopeVersion.Soap12, System.ServiceModel.Channels.AddressingVersion.None); result.Elements.Add(textBindingElement); System.ServiceModel.Channels.HttpsTransportBindingElement httpsBindingElement = new System.ServiceModel.Channels.HttpsTransportBindingElement(); httpsBindingElement.AllowCookies = true; httpsBindingElement.MaxBufferSize = int.MaxValue; httpsBindingElement.MaxReceivedMessageSize = int.MaxValue; result.Elements.Add(httpsBindingElement); return result; } throw new System.InvalidOperationException(string.Format("Could not find endpoint with name \'{0}\'.", endpointConfiguration)); } private static System.ServiceModel.EndpointAddress GetEndpointAddress(EndpointConfiguration endpointConfiguration) { if ((endpointConfiguration == EndpointConfiguration.WayBillsSoap)) { return new System.ServiceModel.EndpointAddress("https://services.rs.ge/WayBillService/WayBillService.asmx"); } if ((endpointConfiguration == EndpointConfiguration.WayBillsSoap12)) { return new System.ServiceModel.EndpointAddress("https://services.rs.ge/WayBillService/WayBillService.asmx"); } throw new System.InvalidOperationException(string.Format("Could not find endpoint with name \'{0}\'.", endpointConfiguration)); } public enum EndpointConfiguration { WayBillsSoap, WayBillsSoap12, } }
Options类:
public class RevenueServiceOptions { public string? ServiceUrl { get; set; } public string? ServiceUser { get; init; } public string? ServicePassword { get; init; } internal void Validate() { if (string.IsNullOrWhiteSpace(ServiceUrl)) throw new Exception($"{nameof(RevenueServiceOptions)}.{nameof(ServiceUrl)} should not be empty"); if (string.IsNullOrWhiteSpace(ServiceUser)) throw new Exception($"{nameof(RevenueServiceOptions)}.{nameof(ServiceUser)} should not be empty"); if (string.IsNullOrWhiteSpace(ServicePassword)) throw new Exception($"{nameof(RevenueServiceOptions)}.{nameof(ServicePassword)} should not be empty"); } }
依赖注入逻辑:
public static class DependencyInjection { public static void AddExternalServices(this IServiceCollection services, RevenueServiceOptions revenueServiceOptions) { services.AddRevenueServiceOptions(revenueServiceOptions); services.AddServices(revenueServiceOptions); services.ConfigureAutoMapper(); } private static void AddRevenueServiceOptions(this IServiceCollection services, RevenueServiceOptions revenueServiceOptions) { if (revenueServiceOptions == null) throw new ArgumentNullException(nameof(revenueServiceOptions)); revenueServiceOptions.Validate(); services.AddSingleton(revenueServiceOptions); } private static void AddServices(this IServiceCollection services, RevenueServiceOptions revenueServiceOptions) { services.AddScoped<WayBillsSoap>(_ => new WayBillsSoapClient(WayBillsSoapClient.EndpointConfiguration.WayBillsSoap, revenueServiceOptions.ServiceUrl)); } private static void ConfigureAutoMapper(this IServiceCollection services) { var mappingConfig = new MapperConfiguration(mc => { mc.AddProfile(new AutoMapper()); }); var mapper = mappingConfig.CreateMapper(); services.AddSingleton(mapper); } }
serviceUrl、serviceUser和servicePassword从appsettings.json读取
请问我哪里配置有误?是否需要额外配置才能发送POST请求?能否推荐有效的学习资源?另外,ConfigureEndpoint分部方法该如何使用?
一、可能的配置错误点
- 未配置服务认证凭证
你定义了ServiceUser和ServicePassword,但依赖注入创建WayBillsSoapClient时完全未使用这些凭证。SOAP服务大概率需要Basic认证,需补充凭证配置:
services.AddScoped<WayBillsSoap>(sp => { var options = sp.GetRequiredService<RevenueServiceOptions>(); var client = new WayBillsSoapClient(WayBillsSoapClient.EndpointConfiguration.WayBillsSoap, options.ServiceUrl); // 添加Basic认证凭证 client.ClientCredentials.UserName.UserName = options.ServiceUser; client.ClientCredentials.UserName.Password = options.ServicePassword; return client; });
- 客户端生命周期管理问题
WayBillsSoapClient实现了ICommunicationObject,若之前请求导致对象进入Faulted状态,后续复用会抛出CommunicationObjectFaultedException。建议封装客户端工厂管理实例生命周期:
public class WayBillsSoapClientFactory { private readonly RevenueServiceOptions _options; public WayBillsSoapClientFactory(RevenueServiceOptions options) { _options = options; } public WayBillsSoap CreateClient() { var client = new WayBillsSoapClient(WayBillsSoapClient.EndpointConfiguration.WayBillsSoap, _options.ServiceUrl); client.ClientCredentials.UserName.UserName = _options.ServiceUser; client.ClientCredentials.UserName.Password = _options.ServicePassword; return client; } } // 注入工厂 services.AddSingleton<WayBillsSoapClientFactory>(); // 使用示例 var client = _factory.CreateClient(); try { var response = await client.get_error_codesAsync(request); // 处理响应 } finally { ((ICommunicationObject)client).Close(); }
- SOAP版本与绑定不匹配
尝试切换到WayBillsSoap12端点测试,验证是否因SOAP版本差异导致通信失败。
二、POST请求的额外配置
svcutil生成的客户端默认使用POST请求调用SOAP服务,无需额外配置,但需验证两点:
- SOAPAction头正确性:客户端应自动添加
OperationContract中指定的Action="http://tempuri.org/get_error_codes"头,可通过开启WCF日志或抓包工具确认。 - 请求XML结构一致性:用Fiddler或Wireshark抓包,对比客户端发送的请求与Postman请求的XML结构、命名空间差异。
三、学习资源推荐
- CoreWCF官方文档:重点关注客户端配置、认证、故障排查章节
- 《Learning WCF》:经典WCF入门书籍,大部分概念在CoreWCF中通用
- Microsoft Learn WCF/CoreWCF教程:系统讲解客户端创建、配置、生命周期管理
四、ConfigureEndpoint分部方法的使用
该方法用于客户端初始化时统一自定义端点配置,需在同一命名空间下创建部分类实现:
namespace RevenueService { public partial class WayBillsSoapClient { static partial void ConfigureEndpoint(System.ServiceModel.Description.ServiceEndpoint serviceEndpoint, System.ServiceModel.Description.ClientCredentials clientCredentials) { // 修改绑定超时时间 if (serviceEndpoint.Binding is BasicHttpBinding binding) { binding.SendTimeout = TimeSpan.FromSeconds(30); binding.ReceiveTimeout = TimeSpan.FromSeconds(30); } // 配置认证(也可在此统一设置凭证) clientCredentials.UserName.UserName = "your_user"; clientCredentials.UserName.Password = "your_pass"; // 添加自定义行为(如消息检查器) serviceEndpoint.Behaviors.Add(new CustomMessageInspector()); } } }
创建WayBillsSoapClient实例时,此方法会自动执行,统一配置所有客户端实例的端点属性。
内容的提问来源于stack exchange,提问作者ramaz bluashvili

