mcpField
H
httpclient-factory

httpclient-factory

github.com/codewithmukesh开发7913.0k 次安装12 周前更新
代码仓库

关于 httpclient-factory

面向发起外部 HTTP 请求的 .NET 开发者的 HttpClient Factory 技能,介绍客户端生命周期管理、弹性处理器,以及用于横切关注点的委托处理器。

主要功能

  1. 01由工厂管理 HTTP 客户端
  2. 02注册键控客户端
  3. 03标准弹性处理器
  4. 04用于认证与关联 ID 的委托处理器
  5. 05使用模拟处理器测试

适用场景

  1. 01重试调用外部 API
  2. 02为请求添加认证令牌
  3. 03传播关联 ID
  4. 04测试 HTTP 集成

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill httpclient-factory

安装后在 Claude Code 中输入/httpclient-factory确认已加载。

详细介绍

HttpClient 工厂

核心原则

  1. 不要为每个请求都 new HttpClient() — 在高负载下,直接创建 HttpClient 会导致套接字耗尽,并忽略 DNS 变更。使用 IHttpClientFactory 管理处理程序的生命周期。
  2. 优先使用键控客户端,而非类型化客户端 — 键控 DI(.AddAsKeyed())是 .NET 10 中推荐的模式。被单例捕获的类型化客户端会悄无声息地导致处理程序轮换失效。
  3. 弹性处理不可或缺 — 每个外部 HTTP 调用都需要重试、断路器和超时。AddStandardResilienceHandler() 一行即可提供合理的默认配置。
  4. 使用 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