httpclient-factory

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

HttpClient Factory

HttpClient Factory

Core Principles

核心原则

  1. Never
    new HttpClient()
    per request
    — Raw
    HttpClient
    creation causes socket exhaustion under load and ignores DNS changes. Use
    IHttpClientFactory
    to manage handler lifetimes.
  2. Keyed clients over typed clients — Keyed DI (
    .AddAsKeyed()
    ) is the recommended pattern in .NET 10. Typed clients captured in singletons silently break handler rotation.
  3. Resilience is not optional — Every external HTTP call needs retry, circuit breaker, and timeout.
    AddStandardResilienceHandler()
    provides sensible defaults in one line.
  4. DelegatingHandlers for cross-cutting concerns — Auth tokens, correlation IDs, and logging belong in the handler pipeline, not scattered across service methods.
  1. 切勿为每个请求
    new HttpClient()
    —— 直接创建
    HttpClient
    会在高负载下导致套接字耗尽,并且忽略DNS变更。使用
    IHttpClientFactory
    来管理处理器(handler)的生命周期。
  2. 优先使用键控客户端而非类型化客户端 —— .NET 10 中推荐使用键控依赖注入(
    .AddAsKeyed()
    )模式。被单例捕获的类型化客户端会静默破坏处理器轮换机制。
  3. 弹性能力不可或缺 —— 每个外部HTTP调用都需要重试、断路器和超时机制。
    AddStandardResilienceHandler()
    仅需一行代码即可提供合理的默认配置。
  4. 使用DelegatingHandlers处理横切关注点 —— 认证令牌、关联ID和日志记录应放在处理器管道中,而非分散在各个服务方法里。

Patterns

模式

Named Client with Resilience

带弹性能力的命名客户端

csharp
builder.Services.AddHttpClient("github", client =>
{
    client.BaseAddress = new Uri("https://api.github.com/");
    client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp/1.0");
    client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/json"));
})
.AddStandardResilienceHandler();

// Usage via factory
public sealed class GitHubService(IHttpClientFactory factory)
{
    public async Task<Repo?> GetRepoAsync(string owner, string name, CancellationToken ct)
    {
        var client = factory.CreateClient("github");
        return await client.GetFromJsonAsync<Repo>($"repos/{owner}/{name}", ct);
    }
}
csharp
builder.Services.AddHttpClient("github", client =>
{
    client.BaseAddress = new Uri("https://api.github.com/");
    client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp/1.0");
    client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/json"));
})
.AddStandardResilienceHandler();

// 通过工厂使用
public sealed class GitHubService(IHttpClientFactory factory)
{
    public async Task<Repo?> GetRepoAsync(string owner, string name, CancellationToken ct)
    {
        var client = factory.CreateClient("github");
        return await client.GetFromJsonAsync<Repo>($"repos/{owner}/{name}", ct);
    }
}

Keyed Client (Recommended in .NET 10)

键控客户端(.NET 10 推荐)

Combines named client configurability with direct injection. No string lookups.
csharp
builder.Services.AddHttpClient("payments", client =>
{
    client.BaseAddress = new Uri("https://api.payments.example.com/");
})
.AddStandardResilienceHandler()
.AddAsKeyed();  // Register as keyed scoped service

// Inject directly — no IHttpClientFactory needed
app.MapPost("/charge", async (
    [FromKeyedServices("payments")] HttpClient httpClient,
    ChargeRequest request,
    CancellationToken ct) =>
{
    var response = await httpClient.PostAsJsonAsync("charges", request, ct);
    return response.IsSuccessStatusCode
        ? TypedResults.Ok()
        : TypedResults.Problem("Payment failed");
});
Global opt-in:
builder.Services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());
结合了命名客户端的可配置性与直接注入特性,无需字符串查找。
csharp
builder.Services.AddHttpClient("payments", client =>
{
    client.BaseAddress = new Uri("https://api.payments.example.com/");
})
.AddStandardResilienceHandler()
.AddAsKeyed();  // 注册为键控作用域服务

// 直接注入 —— 无需IHttpClientFactory
app.MapPost("/charge", async (
    [FromKeyedServices("payments")] HttpClient httpClient,
    ChargeRequest request,
    CancellationToken ct) =>
{
    var response = await httpClient.PostAsJsonAsync("charges", request, ct);
    return response.IsSuccessStatusCode
        ? TypedResults.Ok()
        : TypedResults.Problem("Payment failed");
});
全局启用:
builder.Services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());

Standard Resilience Handler

标准弹性处理器

AddStandardResilienceHandler()
chains 5 strategies:
StrategyDefault
Rate limiter1000 concurrent requests
Total timeout30 seconds
Retry3 retries, exponential backoff with jitter
Circuit breakerOpens at 10% failure rate
Attempt timeout10 seconds per attempt
csharp
builder.Services.AddHttpClient("api")
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 5;
        options.Retry.Delay = TimeSpan.FromSeconds(1);
        options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60);
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(15);

        // Disable retries for non-idempotent methods
        options.Retry.DisableForUnsafeHttpMethods();
    });
AddStandardResilienceHandler()
串联了5种策略:
策略默认值
速率限制器1000个并发请求
总超时30秒
重试3次重试,带抖动的指数退避
断路器失败率达10%时开启
单次尝试超时每次尝试10秒
csharp
builder.Services.AddHttpClient("api")
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 5;
        options.Retry.Delay = TimeSpan.FromSeconds(1);
        options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60);
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(15);

        // 对非幂等方法禁用重试
        options.Retry.DisableForUnsafeHttpMethods();
    });

DelegatingHandler for Auth Token Injection

用于注入认证令牌的DelegatingHandler

csharp
public sealed class AuthenticationHandler(ITokenService tokenService)
    : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await tokenService.GetAccessTokenAsync(cancellationToken);
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return await base.SendAsync(request, cancellationToken);
    }
}

// Registration
builder.Services.AddTransient<AuthenticationHandler>();
builder.Services.AddHttpClient("api")
    .AddHttpMessageHandler<AuthenticationHandler>()
    .AddStandardResilienceHandler();
csharp
public sealed class AuthenticationHandler(ITokenService tokenService)
    : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await tokenService.GetAccessTokenAsync(cancellationToken);
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return await base.SendAsync(request, cancellationToken);
    }
}

// 注册
builder.Services.AddTransient<AuthenticationHandler>();
builder.Services.AddHttpClient("api")
    .AddHttpMessageHandler<AuthenticationHandler>()
    .AddStandardResilienceHandler();

DelegatingHandler for Correlation ID Propagation

用于传播关联ID的DelegatingHandler

csharp
public sealed class CorrelationIdHandler(IHttpContextAccessor httpContextAccessor)
    : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (httpContextAccessor.HttpContext?.Request.Headers
                .TryGetValue("X-Correlation-Id", out var correlationId) is true)
        {
            request.Headers.Add("X-Correlation-Id", correlationId.ToString());
        }
        return base.SendAsync(request, cancellationToken);
    }
}
csharp
public sealed class CorrelationIdHandler(IHttpContextAccessor httpContextAccessor)
    : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (httpContextAccessor.HttpContext?.Request.Headers
                .TryGetValue("X-Correlation-Id", out var correlationId) is true)
        {
            request.Headers.Add("X-Correlation-Id", correlationId.ToString());
        }
        return base.SendAsync(request, cancellationToken);
    }
}

SocketsHttpHandler Configuration

SocketsHttpHandler配置

csharp
builder.Services.AddHttpClient("advanced")
    .UseSocketsHttpHandler((handler, _) =>
    {
        handler.PooledConnectionLifetime = TimeSpan.FromMinutes(2);
        handler.PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1);
        handler.MaxConnectionsPerServer = 100;
        handler.AutomaticDecompression =
            DecompressionMethods.GZip | DecompressionMethods.Brotli;
    });
csharp
builder.Services.AddHttpClient("advanced")
    .UseSocketsHttpHandler((handler, _) =>
    {
        handler.PooledConnectionLifetime = TimeSpan.FromMinutes(2);
        handler.PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1);
        handler.MaxConnectionsPerServer = 100;
        handler.AutomaticDecompression =
            DecompressionMethods.GZip | DecompressionMethods.Brotli;
    });

Testing with Mock Handler

使用Mock Handler进行测试

csharp
public sealed class MockHttpHandler(
    HttpStatusCode statusCode,
    string content) : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return Task.FromResult(new HttpResponseMessage(statusCode)
        {
            Content = new StringContent(content, Encoding.UTF8, "application/json")
        });
    }
}

// In test
var handler = new MockHttpHandler(HttpStatusCode.OK, """{"id":1}""");
var client = new HttpClient(handler) { BaseAddress = new Uri("https://api.test/") };
var service = new MyService(client);
csharp
public sealed class MockHttpHandler(
    HttpStatusCode statusCode,
    string content) : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return Task.FromResult(new HttpResponseMessage(statusCode)
        {
            Content = new StringContent(content, Encoding.UTF8, "application/json")
        });
    }
}

// 在测试中
var handler = new MockHttpHandler(HttpStatusCode.OK, """{"id":1}""");
var client = new HttpClient(handler) { BaseAddress = new Uri("https://api.test/") };
var service = new MyService(client);

Anti-patterns

反模式

Don't Create HttpClient Per Request

切勿为每个请求创建HttpClient

csharp
// BAD — socket exhaustion under load, ignores DNS changes
public async Task<string> GetDataAsync()
{
    using var client = new HttpClient();
    return await client.GetStringAsync("https://api.example.com/data");
}

// GOOD — factory-managed
public async Task<string> GetDataAsync(CancellationToken ct)
{
    var client = factory.CreateClient("api");
    return await client.GetStringAsync("https://api.example.com/data", ct);
}
csharp
// 错误示例 —— 高负载下会导致套接字耗尽,忽略DNS变更
public async Task<string> GetDataAsync()
{
    using var client = new HttpClient();
    return await client.GetStringAsync("https://api.example.com/data");
}

// 正确示例 —— 由工厂管理
public async Task<string> GetDataAsync(CancellationToken ct)
{
    var client = factory.CreateClient("api");
    return await client.GetStringAsync("https://api.example.com/data", ct);
}

Don't Capture Typed Clients in Singletons

切勿在单例中捕获类型化客户端

csharp
// BAD — transient HttpClient captured by singleton defeats handler rotation
services.AddSingleton<MySingletonService>();
services.AddHttpClient<MySingletonService>();

// GOOD — use keyed client or IHttpClientFactory in singletons
services.AddSingleton<MySingletonService>();
services.AddHttpClient("myservice").AddAsKeyed(ServiceLifetime.Singleton);
csharp
// 错误示例 —— 瞬态HttpClient被单例捕获会破坏处理器轮换
services.AddSingleton<MySingletonService>();
services.AddHttpClient<MySingletonService>();

// 正确示例 —— 在单例中使用命名客户端或键控客户端
services.AddSingleton<MySingletonService>();
services.AddHttpClient("myservice").AddAsKeyed(ServiceLifetime.Singleton);

Don't Mutate DefaultRequestHeaders on Shared Clients

切勿在共享客户端上修改DefaultRequestHeaders

csharp
// BAD — not thread-safe
httpClient.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", token);

// GOOD — use DelegatingHandler or per-request HttpRequestMessage
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/data");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
await httpClient.SendAsync(request, ct);
csharp
// 错误示例 —— 非线程安全
httpClient.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", token);

// 正确示例 —— 使用DelegatingHandler或每个请求的HttpRequestMessage
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/data");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
await httpClient.SendAsync(request, ct);

Don't Forget CancellationToken

切勿忘记CancellationToken

csharp
// BAD — no cancellation support
var result = await httpClient.GetFromJsonAsync<Order>("/orders/1");

// GOOD — always pass CancellationToken
var result = await httpClient.GetFromJsonAsync<Order>("/orders/1", cancellationToken);
csharp
// 错误示例 —— 不支持取消操作
var result = await httpClient.GetFromJsonAsync<Order>("/orders/1");

// 正确示例 —— 始终传递CancellationToken
var result = await httpClient.GetFromJsonAsync<Order>("/orders/1", cancellationToken);

Don't Stack Multiple Resilience Handlers

切勿堆叠多个弹性处理器

csharp
// BAD — conflicting resilience strategies
builder.AddStandardResilienceHandler();
builder.AddStandardHedgingHandler();

// GOOD — one standard handler, or a custom pipeline
builder.AddStandardResilienceHandler();
csharp
// 错误示例 —— 弹性策略冲突
builder.AddStandardResilienceHandler();
builder.AddStandardHedgingHandler();

// 正确示例 —— 使用一个标准处理器,或自定义管道
builder.AddStandardResilienceHandler();

Decision Guide

决策指南

ScenarioRecommendation
New .NET 10 projectKeyed clients with
AddAsKeyed()
Singleton service needs HttpClientNamed client via
IHttpClientFactory
or keyed singleton
External API calls
AddStandardResilienceHandler()
on every client
Auth token injection
DelegatingHandler
registered with
AddHttpMessageHandler
Hedging (parallel requests)
AddStandardHedgingHandler()
for latency-sensitive calls
Non-idempotent methods
DisableForUnsafeHttpMethods()
on retry options
Custom retry logic
AddResilienceHandler("name", builder => ...)
Connection pooling control
UseSocketsHttpHandler
with
PooledConnectionLifetime
API client generationRefit with
AddRefitClient<T>()
Integration testingCustom
HttpMessageHandler
or
MockHttpMessageHandler
场景推荐方案
新的.NET 10项目使用
AddAsKeyed()
的键控客户端
单例服务需要HttpClient通过
IHttpClientFactory
使用命名客户端,或键控单例
外部API调用为每个客户端添加
AddStandardResilienceHandler()
认证令牌注入使用
AddHttpMessageHandler()
注册
DelegatingHandler
对冲(并行请求)对延迟敏感的调用使用
AddStandardHedgingHandler()
非幂等方法在重试选项上使用
DisableForUnsafeHttpMethods()
自定义重试逻辑使用
AddResilienceHandler("name", builder => ...)
连接池控制使用
UseSocketsHttpHandler
配置
PooledConnectionLifetime
API客户端生成使用
AddRefitClient<T>()
结合Refit
集成测试使用自定义
HttpMessageHandler
MockHttpMessageHandler