关于 httpclient-factory
面向发起外部 HTTP 请求的 .NET 开发者的 HttpClient Factory 技能,介绍客户端生命周期管理、弹性处理器,以及用于横切关注点的委托处理器。
主要功能
- 01由工厂管理 HTTP 客户端
- 02注册键控客户端
- 03标准弹性处理器
- 04用于认证与关联 ID 的委托处理器
- 05使用模拟处理器测试
适用场景
- 01重试调用外部 API
- 02为请求添加认证令牌
- 03传播关联 ID
- 04测试 HTTP 集成
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill httpclient-factory安装后在 Claude Code 中输入/httpclient-factory确认已加载。
详细介绍
HttpClient 工厂
核心原则
- 不要为每个请求都
new HttpClient()— 在高负载下,直接创建HttpClient会导致套接字耗尽,并忽略 DNS 变更。使用IHttpClientFactory管理处理程序的生命周期。 - 优先使用键控客户端,而非类型化客户端 — 键控 DI(
.AddAsKeyed())是 .NET 10 中推荐的模式。被单例捕获的类型化客户端会悄无声息地导致处理程序轮换失效。 - 弹性处理不可或缺 — 每个外部 HTTP 调用都需要重试、断路器和超时。
AddStandardResilienceHandler()一行即可提供合理的默认配置。 - 使用 DelegatingHandler 处理横切关注点 — 身份验证令牌、关联 ID 和日志记录应放在处理程序管道中,而不是散布在各个服务方法里。
模式
带弹性处理的命名客户端
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);
}
}
键控客户端(.NET 10 推荐)
兼具命名客户端的可配置性和直接注入,无需字符串查找。
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");
});
全局启用:builder.Services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());
标准弹性处理程序
AddStandardResilienceHandler() 会串联 5 种策略:
| 策略 | 默认值 |
|---|---|
| 速率限制器 | 1000 个并发请求 |
| 总超时 | 30 秒 |
| 重试 | 重试 3 次,采用带抖动的指数退避 |
| 断路器 | 失败率达到 10% 时打开 |
| 单次尝试超时 | 每次尝试 10 秒 |
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();
});
用于注入身份验证令牌的 DelegatingHandler
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();
用于传播关联 ID 的 DelegatingHandler
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 配置
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;
});
使用模拟处理程序进行测试
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);
反模式
不要为每个请求都创建 HttpClient
// 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);
}
不要在单例中捕获类型化客户端
// 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);
不要修改共享客户端的 DefaultRequestHeaders
// 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);
不要忘记 CancellationToken
// 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);
不要叠加多个弹性处理程序
// BAD — conflicting resilience strategies
builder.AddStandardResilienceHandler();
builder.AddStandardHedgingHandler();
// GOOD — one standard handler, or a custom pipeline
builder.AddStandardResilienceHandler();
决策指南
| 场景 | 建议 |
|---|---|
| 新建 .NET 10 项目 | 使用 AddAsKeyed() 的键控客户端 |
| 单例服务需要 HttpClient | 通过 IHttpClientFactory 使用命名客户端,或使用键控单例 |
| 调用外部 API | 在每个客户端上使用 AddStandardResilienceHandler() |
| 注入身份验证令牌 | 注册到 AddHttpMessageHandler 的 DelegatingHandler |
| 对冲(并行请求) | 对延迟敏感的调用使用 AddStandardHedgingHandler() |
| 非幂等方法 | 在重试选项上使用 DisableForUnsafeHttpMethods() |
| 自定义重试逻辑 | 使用 AddResilienceHandler("name", builder => ...) |
| 控制连接池 | 使用带 PooledConnectionLifetime 的 UseSocketsHttpHandler |
| 生成 API 客户端 | 使用 Refit 的 AddRefitClient<T>() |
| 集成测试 | 自定义 HttpMessageHandler 或 MockHttpMessageHandler |
评论
支持 Markdown