httpclient-factory
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHttpClient Factory
HttpClient Factory
Core Principles
核心原则
- Never per request — Raw
new HttpClient()creation causes socket exhaustion under load and ignores DNS changes. UseHttpClientto manage handler lifetimes.IHttpClientFactory - Keyed clients over typed clients — Keyed DI () is the recommended pattern in .NET 10. Typed clients captured in singletons silently break handler rotation.
.AddAsKeyed() - Resilience is not optional — Every external HTTP call needs retry, circuit breaker, and timeout. provides sensible defaults in one line.
AddStandardResilienceHandler() - DelegatingHandlers for cross-cutting concerns — Auth tokens, correlation IDs, and logging belong in the handler pipeline, not scattered across service methods.
- 切勿为每个请求—— 直接创建
new HttpClient()会在高负载下导致套接字耗尽,并且忽略DNS变更。使用HttpClient来管理处理器(handler)的生命周期。IHttpClientFactory - 优先使用键控客户端而非类型化客户端 —— .NET 10 中推荐使用键控依赖注入()模式。被单例捕获的类型化客户端会静默破坏处理器轮换机制。
.AddAsKeyed() - 弹性能力不可或缺 —— 每个外部HTTP调用都需要重试、断路器和超时机制。仅需一行代码即可提供合理的默认配置。
AddStandardResilienceHandler() - 使用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()| Strategy | Default |
|---|---|
| Rate limiter | 1000 concurrent requests |
| Total timeout | 30 seconds |
| Retry | 3 retries, exponential backoff with jitter |
| Circuit breaker | Opens at 10% failure rate |
| Attempt timeout | 10 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()| 策略 | 默认值 |
|---|---|
| 速率限制器 | 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
决策指南
| Scenario | Recommendation |
|---|---|
| New .NET 10 project | Keyed clients with |
| Singleton service needs HttpClient | Named client via |
| External API calls | |
| Auth token injection | |
| Hedging (parallel requests) | |
| Non-idempotent methods | |
| Custom retry logic | |
| Connection pooling control | |
| API client generation | Refit with |
| Integration testing | Custom |
| 场景 | 推荐方案 |
|---|---|
| 新的.NET 10项目 | 使用 |
| 单例服务需要HttpClient | 通过 |
| 外部API调用 | 为每个客户端添加 |
| 认证令牌注入 | 使用 |
| 对冲(并行请求) | 对延迟敏感的调用使用 |
| 非幂等方法 | 在重试选项上使用 |
| 自定义重试逻辑 | 使用 |
| 连接池控制 | 使用 |
| API客户端生成 | 使用 |
| 集成测试 | 使用自定义 |