You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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分部方法该如何使用?


问题排查与解决方案

一、可能的配置错误点

  1. 未配置服务认证凭证
    你定义了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;
});
  1. 客户端生命周期管理问题
    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();
}
  1. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 10:15:47