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

如何用HttpClientFactory创建高效多客户端HttpClient并实现代码复用?

问题描述

我用HttpClientFactory创建HttpClient,现在要对接多个外部客户端,想把重复的请求逻辑集中管理减少冗余,但不确定高请求量场景下这种中心化实现是否符合最佳实践,同时想知道怎么在保证可扩展性的同时避免重复代码。

当前代码里ApiClientOne和ApiClientTwo有完全相同的私有Request方法,我想把这个方法移到公共位置让N个客户端调用,有三个疑问:

  1. 将async Task<T> Request<T>(HttpMethod method, string endpoint, object payload = null)移到公共位置供多客户端调用,是否具备良好的可扩展性?
  2. 是否有更优的实现方案?
  3. 是否应该保持当前的独立实现方式?

代码示例

Program.cs

.ConfigureServices((_, services) =>
{
    services.AddApiClientOne(config);
    services.AddApiClientTwo(config);
})

ServiceContainer.cs

namespace Configuration
{
    [ExcludeFromCodeCoverage]
    public static class ServiceContainer
    {
        public static void AddApiClientTwo(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddHttpClient<IApiClientTwo, ApiClientTwo>((serviceProvider, httpClient) =>
            {
                httpClient.BaseAddress = new Uri(configuration.GetSettings("Services:ApiClientTwo:BaseUrl", ""));
                httpClient.Timeout = TimeSpan.FromSeconds(30);
                httpClient.DefaultRequestHeaders.Add("Accept", "application/json");

                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer",
                    serviceProvider.GetRequiredService<IInternalAuthClient>().GetTokenAsync()
                        .GetAwaiter().GetResult());
            });
        }

        public static void AddApiClientOne(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddHttpClient<IApiClientOne, ApiClientOne>("ApiClientOne", options =>
            {
                options.BaseAddress = new Uri(configuration.GetSettings("Services:ClientOne:BaseUrl", string.Empty, false));
                options.DefaultRequestHeaders.Add("clientId", configuration[Configurations.ClientOneClientId]);
                options.SetBasicAuthentication(configuration[Configurations.ClientOneUsername],
                    configuration[Configurations.ClientOnePassword]);
            });
            
        }

     }
}

ApiClientOne.cs

namespace Clients.ApiClientOne;

public class ApiClientOne : IApiClientOne
{
    private readonly ILogger<ApiClientOne> _logger;
    private readonly HttpClient _httpClient;
    private Lazy<JsonSerializerOptions> _settings;
    
    public ApiClientOne(HttpClient httpClient, ILogger<ApiClientOne> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
        _settings = new Lazy<JsonSerializerOptions>(CreateSerializerSettings);
    }
    
    public async Task<GetSomethingResponse> GetSomething(string value)
    {
        var response = await Request<GetSomethingResponse>(HttpMethod.Get, $"/something/{value}");
        return response;
    }
    
    private JsonSerializerOptions CreateSerializerSettings()
    {
        var settings = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        return settings;
    }

    private async Task<T> Request<T>(HttpMethod method, string endpoint, object payload = null)
    {
        var client = _httpClient;
        var disposeClient = false;

        using var request = new HttpRequestMessage();
        var content_ = new StringContent(JsonSerializer.Serialize(payload, _settings.Value));
        content_.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
        request.Content = content_;
        request.Method = method;
        request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
        request.RequestUri = new Uri(endpoint, UriKind.RelativeOrAbsolute);

        var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
        var disposeResponse_ = true;
        
        try
        {
            var content = await response.Content.ReadAsStringAsync();
            var headers =
                Enumerable.ToDictionary(response.Headers, h_ => h_.Key, h_ => h_.Value);
            if (response.Content != null && response.Content.Headers != null)
            {
                foreach (var item in response.Content.Headers)
                    headers[item.Key] = item.Value;
            }

            if (!response.IsSuccessStatusCode)
            {
                var errorMessage = $"some error.";
                _logger.Error($"{errorMessage} {{ @content }}", args: new object[] {content});
                throw new HttpRequestException($"{errorMessage} - {content}", null, response.StatusCode);
            }

            return JsonSerializer.Deserialize<T>(content);
        }
        catch (HttpRequestException ex) when (ex.StatusCode.HasValue && 500 <= (int) ex.StatusCode &&
                                              (int) ex.StatusCode < 600)
        {
            throw new DependencyFailureApiException(ex.Message);
        }
        finally
        {
            if (disposeResponse_)
            {
                response.Dispose();
                client.Dispose();
            }
        }
    }
}

ApiClientTwo.cs

namespace Clients.ApiClientTwo;

public class ApiClientTwo : IApiClientTwo
{
    private readonly ILogger<ApiClientTwo> _logger;
    private readonly HttpClient _httpClient;
    private Lazy<JsonSerializerOptions> _settings;
    
    public ApiClientTwo(HttpClient httpClient, ILogger<ApiClientTwo> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
        _settings = new Lazy<JsonSerializerOptions>(CreateSerializerSettings);
    }
    
    public async Task<PostSomethingResponse> PostSomething(MyModel value)
    {
        var response = await Request<PostSomethingResponse>(HttpMethod.Post, $"/postSomething", value);
        return response;
    }
    
    private JsonSerializerOptions CreateSerializerSettings()
    {
        var settings = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        return settings;
    }

    private async Task<T> Request<T>(HttpMethod method, string endpoint, object payload = null)
    {
        var client = _httpClient;
        var disposeClient = false;

        using var request = new HttpRequestMessage();
        var content_ = new StringContent(JsonSerializer.Serialize(payload, _settings.Value));
        content_.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
        request.Content = content_;
        request.Method = method;
        request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
        request.RequestUri = new Uri(endpoint, UriKind.RelativeOrAbsolute);

        var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
        var disposeResponse_ = true;

        try
        {
            var content = await response.Content.ReadAsStringAsync();
            var headers =
                Enumerable.ToDictionary(response.Headers, h_ => h_.Key, h_ => h_.Value);
            if (response.Content != null && response.Content.Headers != null)
            {
                foreach (var item in response.Content.Headers)
                    headers[item.Key] = item.Value;
            }

            if (!response.IsSuccessStatusCode)
            {
                var errorMessage = $"some error.";
                _logger.Error($"{errorMessage} {{ @content }}", args: new object[] { content });
                throw new HttpRequestException($"{errorMessage} - {content}", null, response.StatusCode);
            }

            return JsonSerializer.Deserialize<T>(content);
        }
        catch (HttpRequestException ex) when (ex.StatusCode.HasValue && 500 <= (int)ex.StatusCode &&
                                              (int)ex.StatusCode < 600)
        {
            throw new DependencyFailureApiException(ex.Message);
        }
        finally
        {
            if (disposeResponse_)
            {
                response.Dispose();
                client.Dispose();
            }
        }
    }
}

问题解答

1. 公共Request方法的可扩展性

把Request方法移到公共位置具备良好的可扩展性,关键是选对实现方式:

  • 用抽象基类:定义BaseApiClient封装通用逻辑(Request方法、序列化配置、Logger、HttpClient),子类继承后只需实现业务接口方法。这种方式允许子类重写部分逻辑(比如特殊序列化规则、自定义错误处理),灵活性足够覆盖绝大多数场景。
  • 用静态扩展方法:把Request做成HttpClient的扩展,传入Logger和序列化配置即可。这种方式更轻量,但灵活性稍弱,适合所有客户端逻辑完全统一的场景。

高请求量场景下,这种中心化实现不会有性能问题——HttpClientFactory本身就是为高并发设计的,公共方法只是复用逻辑,不会影响连接池管理,反而因为代码统一,更容易批量优化(比如添加统一的重试、熔断策略)。

2. 更优的实现方案

推荐两种方案,按实用性排序:

方案一:抽象基类+依赖注入

定义抽象基类封装通用逻辑,子类专注业务实现:

public abstract class BaseApiClient<TClient>
{
    protected readonly HttpClient _httpClient;
    protected readonly ILogger<TClient> _logger;
    protected readonly Lazy<JsonSerializerOptions> _serializerOptions;

    protected BaseApiClient(HttpClient httpClient, ILogger<TClient> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
        _serializerOptions = new Lazy<JsonSerializerOptions>(CreateSerializerSettings);
    }

    protected virtual JsonSerializerOptions CreateSerializerSettings()
    {
        return new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
    }

    protected async Task<TResponse> Request<TResponse>(HttpMethod method, string endpoint, object payload = null)
    {
        using var request = new HttpRequestMessage(method, endpoint);
        
        if (payload != null)
        {
            var content = new StringContent(JsonSerializer.Serialize(payload, _serializerOptions.Value));
            content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
            request.Content = content;
        }

        request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));

        var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
        
        try
        {
            var content = await response.Content.ReadAsStringAsync();
            
            if (!response.IsSuccessStatusCode)
            {
                var errorMessage = $"请求失败,状态码:{(int)response.StatusCode}";
                _logger.LogError($"{errorMessage},内容:{content}");
                throw new HttpRequestException($"{errorMessage} - {content}", null, response.StatusCode);
            }

            return JsonSerializer.Deserialize<TResponse>(content, _serializerOptions.Value);
        }
        catch (HttpRequestException ex) when (ex.StatusCode.HasValue && (int)ex.StatusCode >= 500 && (int)ex.StatusCode < 600)
        {
            throw new DependencyFailureApiException(ex.Message);
        }
    }
}

子类继承基类后只需实现业务方法:

public class ApiClientOne : BaseApiClient<ApiClientOne>, IApiClientOne
{
    public ApiClientOne(HttpClient httpClient, ILogger<ApiClientOne> logger) : base(httpClient, logger)
    {
    }

    public async Task<GetSomethingResponse> GetSomething(string value)
    {
        return await Request<GetSomethingResponse>(HttpMethod.Get, $"/something/{value}");
    }
}

重要提醒:原代码里的client.Dispose()必须删掉!HttpClientFactory管理的HttpClient不能手动释放,否则会破坏连接池,导致高并发下性能急剧下降。

方案二:自定义DelegatingHandler处理横切逻辑

如果需要统一处理请求日志、重试、认证等横切逻辑,可以自定义委托处理程序:

public class ApiClientLoggingHandler : DelegatingHandler
{
    private readonly ILogger<ApiClientLoggingHandler> _logger;

    public ApiClientLoggingHandler(ILogger<ApiClientLoggingHandler> logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        _logger.LogInformation("发送请求:{Method} {Uri}", request.Method, request.RequestUri);
        
        var response = await base.SendAsync(request, cancellationToken);
        
        if (!response.IsSuccessStatusCode)
        {
            var content = await response.Content.ReadAsStringAsync();
            _logger.LogError("请求失败:{StatusCode} {Content}", response.StatusCode, content);
        }
        
        return response;
    }
}

注册HttpClient时添加该Handler:

services.AddHttpClient<IApiClientOne, ApiClientOne>()
    .AddHttpMessageHandler<ApiClientLoggingHandler>();

这种方式适合把横切逻辑和业务逻辑分离,配合基类使用效果最佳。

3. 是否保持独立实现?

不建议保持当前的独立实现方式:

  • 重复代码会导致维护成本极高,比如修改错误处理逻辑、序列化配置时,需要在N个客户端里逐一修改,容易遗漏。
  • 高请求量场景下,统一的逻辑更容易批量优化(比如添加统一的重试策略、超时控制),独立实现很难做到逻辑一致。
  • 唯一的例外是某个客户端的请求逻辑完全特殊,和其他客户端没有任何共性,但这种情况极少,大部分场景下都能提取通用逻辑。

内容的提问来源于stack exchange,提问作者GThree

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 02:14:59