Typed Client 是 IHttpClientFactory 提供的强类型封装模式,将 HttpClient 配置与业务逻辑绑定在类中;它不继承 HttpClient,而是通过构造函数注入由工厂管理的实例,避免手动创建导致的连接池、策略、生命周期等问题。

Typed Client 是什么,和普通 HttpClient 有什么区别
Typed Client 不是新类型的客户端,而是 IHttpClientFactory 提供的一种注册与使用模式:把 HttpClient 和它的配置、行为(如重试、认证头)封装进一个强类型类里。它和直接 new HttpClient() 或用 IServiceCollection.AddHttpClient<t>()</t> 注册的普通命名客户端不同——后者只配了客户端实例,而 Typed Client 把“用这个客户端干啥”也一并定义在类型中。
关键点在于:Typed Client 类本身不继承 HttpClient,也不持有 HttpClient 字段,而是通过构造函数接收 HttpClient 实例,由 DI 容器自动注入。这避免了手动管理生命周期,也天然支持依赖注入链中的其他服务(比如 IOptionsMonitor<apisettings></apisettings>)。
如何注册并使用 Typed Client(.NET 6+ 推荐写法)
在 Program.cs 中注册:
builder.Services.AddHttpClient<GitHubService>(client => { client.BaseAddress = new Uri("https://api.github.com/"); client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp/1.0"); });
对应的服务类定义为:
public class GitHubService { private readonly HttpClient _httpClient; <pre class="brush:php;toolbar:false;">public GitHubService(HttpClient httpClient) { _httpClient = httpClient; } public async Task<string> GetRepoNameAsync(string owner, string repo) { var response = await _httpClient.GetAsync($"repos/{owner}/{repo}"); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); }
}
注意:
-
GitHubService不需要标记[ServiceContract]或继承任何基类 - 构造函数参数必须是
HttpClient,不能是IHttpClientFactory或其他变体 - 注册时泛型参数
<githubservice></githubservice>必须和类名完全一致,否则 DI 无法解析
为什么不能在 Typed Client 里 new HttpClient
常见错误是这样写:
public class BadService { private readonly HttpClient _client = new HttpClient(); // ❌ 错误! // ... }
这会导致:
- 绕过
IHttpClientFactory的连接池复用和 DNS 刷新机制 - 没有内置的 Polly 策略(如超时、重试)支持
- 无法利用工厂提供的日志、指标、命名隔离等能力
- 在高并发下容易触发
SocketException: Too many open files(尤其 linux 容器环境)
Typed Client 的核心价值,就是让 HttpClient 实例的创建、配置、销毁全部交由工厂统一管控,业务类只专注逻辑。
多个 Typed Client 共享同一组配置?用命名客户端 + 委托注册
如果几个服务都要访问同一个 API 域名,但又希望各自独立类型(比如 OrderService 和 InventoryService),可以复用命名配置:
builder.Services.AddHttpClient("internal-api", client => { client.BaseAddress = new Uri(builder.Configuration["InternalApi:BaseUrl"]); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", builder.Configuration["InternalApi:Token"]); }); <p>builder.Services.AddTransient<OrderService>(sp => { var httpClient = sp.GetRequiredService<IHttpClientFactory>() .CreateClient("internal-api"); return new OrderService(httpClient); });</p><p>builder.Services.AddTransient<InventoryService>(sp => { var httpClient = sp.GetRequiredService<IHttpClientFactory>() .CreateClient("internal-api"); return new InventoryService(httpClient); }); </p>
这种方式比重复注册更可控,也便于后期切流或打标监控。
Typed Client 看似简单,真正容易出问题的地方在于:以为“只要构造函数有 HttpClient 就算用了工厂”,结果手动 new 了实例,或者混淆了命名客户端与类型化客户端的注册路径。一旦跨服务共享配置或加策略,这些边界就立刻暴露。