在C#后端架构里,外部HTTP服务调用若散落在业务代码中,既难维护又容易引发连接资源问题。将IHttpClientFactory与仓储模式结合,能把HTTP客户端生命周期交给框架托管,同时在仓储层统一封装对第三方接口的访问逻辑。

为什么HttpClient不能直接单例或频繁创建
很多团队初期会把HttpClient做成静态单例,认为这样能复用TCP连接。但HttpClient单例会固化首次解析的DNS结果,当对方服务迁移或换IP时,单例客户端不会自动重新解析,导致请求持续失败。另一方面,如果在每次调用时用new HttpClient()然后不释放,由于HttpClient实现了IDisposable,其底层Socket并不会随Dispose立即回收,操作系统会保留TIME_WAIT状态,高并发下很快耗尽端口。
IHttpClientFactory的设计目标正是解决这两类问题。它在内部维护一个连接池,每次从工厂获取的客户端逻辑实例共享底层Handler栈,同时支持定时轮换主Handler以刷新DNS。这样既能复用连接,又避免了端口泄漏,还能集中配置超时、重试与熔断策略。
在仓储模式中集成IHttpClientFactory的思路
仓储模式强调把数据获取逻辑从业务层剥离。当数据来源是外部HTTP API时,我们可以定义一个接口如IOrderExternalRepository,并在实现中通过构造函数注入IHttpClientFactory。仓储内部决定如何创建请求、反序列化响应,业务服务只依赖仓储接口,不感知HTTP细节。
更进一步的做法是使用Typed Client:为特定外部服务定义一个强类型客户端类,并在Startup或Program中通过AddHttpClient注册,框架会自动将该类的实例注入到仓储构造函数。这样配置和调用都内聚在Typed Client里,仓储只是协调者。下面分别给出基础工厂注入与Typed Client两种写法。
基础工厂注入方式
先注册服务,在Program.cs中添加以下代码:
// 注册IHttpClientFactory,框架内置无需额外包 builder.Services.AddHttpClient(); // 注册仓储 builder.Services.AddScoped<IProductRepository, HttpProductRepository>();
仓储实现中注入工厂并创建客户端:
public interface IProductRepository
{
Task<Product> GetByIdAsync(int id);
}
public class HttpProductRepository : IProductRepository
{
private readonly IHttpClientFactory _factory;
public HttpProductRepository(IHttpClientFactory factory)
{
_factory = factory;
}
public async Task<Product> GetByIdAsync(int id)
{
// 每次从工厂获取命名客户端或默认客户端
var client = _factory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(10);
var response = await client.GetAsync($"https://ipipp.com/api/products/{id}");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<Product>(json);
}
}
这种写法简单直观,适合外部接口较少的场景。缺点是超时等配置散落在仓储方法里,多个方法重复设置容易不一致。
Typed Client方式
Typed Client把配置和调用固化到独立类,注册时代入基类:
public class ProductApiClient
{
private readonly HttpClient _client;
public ProductApiClient(HttpClient client)
{
_client = client;
}
public async Task<Product> GetByIdAsync(int id)
{
var response = await _client.GetAsync($"products/{id}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Product>();
}
}
注册时指定基址与生存周期策略:
builder.Services.AddHttpClient<ProductApiClient>(c =>
{
c.BaseAddress = new Uri("https://ipipp.com/api/");
c.Timeout = TimeSpan.FromSeconds(15);
}).SetHandlerLifetime(TimeSpan.FromMinutes(5));
builder.Services.AddScoped<IProductRepository, ProductApiRepository>();
仓储只依赖ProductApiClient,不直接接触工厂:
public class ProductApiRepository : IProductRepository
{
private readonly ProductApiClient _api;
public ProductApiRepository(ProductApiClient api)
{
_api = api;
}
public Task<Product> GetByIdAsync(int id) => _api.GetByIdAsync(id);
}
这样一来,HTTP连接生命周期由框架按Handler生存周期管理,默认两分钟轮换主Handler以刷新DNS,同时连接池复用底层Socket,仓储层保持干净。
连接生命周期与重试策略配置
SetHandlerLifetime控制底层HttpClientHandler被回收的间隔,到期后会新建Handler并重新解析DNS,旧连接逐渐关闭。对于不稳定外部网络,可叠加Polly策略实现重试与熔断:
builder.Services.AddHttpClient<ProductApiClient>(c =>
{
c.BaseAddress = new Uri("https://ipipp.com/api/");
})
.AddPolicyHandler(GetRetryPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return Policy.Handle<HttpRequestException>()
.OrResult(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(200 * attempt));
}
通过上述配置,短暂网络抖动会被自动重试掩盖,且每次策略执行都运行在工厂管理的客户端上,不会额外占用端口。当服务规模扩大,也可将工厂与仓储一并接入DI容器的作用域,保证一次请求内复用同一逻辑客户端实例。
常见误区与排查建议
一个典型误区是在仓储方法内部用using(var client = new HttpClient()),这比不释放更糟,会频繁创建Handler栈。另一个误区是认为注入的HttpClient就是物理单例,其实工厂返回的是逻辑客户端,共享Handler池。排查连接数异常时,可用netstat观察TIME_WAIT,并检查是否误用了new HttpClient而非工厂。
将IHttpClientFactory与仓储模式结合,不仅规范了外部服务调用的边界,也把连接生命周期、DNS刷新、重试熔断等横切关注点从业务代码里剥离。架构上清晰,运维上也更容易定位HTTP层问题。
IHttpClientFactory仓储模式Http连接生命周期修改时间:2026-08-04 19:30:30