到目前为止,我们一直直接使用HttpClient。在每个服务中,我们都创建了一个HttpClient实例和所有必需的配置。这会导致了重复代码。在这篇文章中,我们将学习如何通过使用HttpClientFactory来改善它。当然,这并不是使用HttpClientFactory的唯一优势。我们将学习HttpClientFactory如何防止HttpClient可能导致的其他问题。此外,我们将展示如何使用HttpClientFactory创建命名和类型化客户端。
HttpClient问题
HttpClient类实现了IDisposable接口。看到这一点,尝试在using指令中使用我们的HttpClient实例,从而在它超出作用域时释放。但是,这并不是一个好的做法。如果我们丢弃了HttpClient,也将丢弃底层的HttpClientHandler。现在,这意味着对于每个新请求,必须创建一个新的HttpClient实例,从而也创建一个处理程序。当然,这就是问题所在。重新打开连接可能会导致性能变慢,因为在使用HttpClient时,这些连接和HttpClientHandler非常昂贵。
此外,还有另一个问题。通过创建太多的连接,可能会面临套接字耗尽,因为过快地使用了太多的套接字,而且没有套接字来创建新的连接。
因此,考虑到所有这些,我们不应该丢弃HttpClient,而是在整个请求中共享它。这就是我们在以前的文章中对静态HttpClient实例所做的事情。这也允许重用底层连接。
但是,我们必须注意,使用静态实例并不是最终的解决方案。当重用实例时,我们也重用连接,直到套接字关闭。为了帮助我们解决这些问题,我们可以使用HttpClientFactory来创建HttpClient实例。
HttpClientFactory如何帮助我们解决上述问题?
HttpClientFactory不仅可以创建和管理新的HttpClient实例,而且还可以与底层处理程序一起工作。当创建新的HttpClient实例时,它不会重新创建消息处理器,而是从池中获取一个。然后,它使用该消息处理程序将请求发送到API。处理程序的默认生存期设置为两分钟,在这段时间内,对新HttpClient的任何请求都可以重用现有的消息处理程序和连接。这意味着我们不必为每个请求创建一个新的消息处理程序,也不必打开一个新的连接,从而防止套接字耗尽问题。
除了解决这些问题,使用HttpClientHandler,我们还可以集中HttpClient的配置。如果你阅读本系列的前几篇文章,会发现我们必须在每个服务类中重复相同的配置。有了HttpClientHandler,我们可以改善这个问题。让我们看看如何使用HttpClientFactory。
添加HttpClientFactory
为了能够在我们的应用程序中使用HttpClientFactory,必须安装 Microsoft.Extensions.Http。
Install-Package Microsoft.Extensions.Http -Version 5.0.0
然后,我们必须使用Program 类中通过AddHttpClient方法将IHttpClientFactory和其他服务添加到服务集合中:
private static void ConfigureServices(IServiceCollection services) { services.AddHttpClient(); //services.AddScoped<IHttpClientServiceImplementation, HttpClientCrudService>(); //services.AddScoped<IHttpClientServiceImplementation, HttpClientPatchService>(); //services.AddScoped<IHttpClientServiceImplementation, HttpClientStreamService>(); //services.AddScoped<IHttpClientServiceImplementation, HttpClientCancellationService>(); }
我们很快就会用额外的配置来扩展这个方法。现在,让我们创建一个新的服务类,就像我们在前面的文章中所做的那样:
public class HttpClientFactoryService : IHttpClientServiceImplementation { private readonly IHttpClientFactory _httpClientFactory; private readonly JsonSerializerOptions _options; public HttpClientFactoryService(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; } public async Task Execute() { throw new NotImplementedException(); } }
为了能够在我们的HttpClientFactoryService类中使用HttpClientFactory,我们必须通过依赖注入来注入它。此外,我们还为JSON序列化配置选项。我们不想在这里添加取消逻辑,所以没有像上一篇文章中那样使用CancellationTokenSource。
现在,让我们添加一个新方法来从API中获取公司数据:
private async Task GetCompaniesWithHttpClientFactory() { var httpClient = _httpClientFactory.CreateClient(); using (var response = await httpClient.GetAsync("https://localhost:5001/api/companies", HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); } }
在这段代码中,我们唯一不熟悉的部分是使用HttpClientFactory中的CreateClient方法来使用默认配置创建一个新的HttpClient。本系列前面的文章中已经解释了其他所有内容。另外,由于没有提供自定义配置,我们必须在GetAsync方法中使用完整的URI。
在此之后,我们可以修改Execute方法:
public async Task Execute(){ await GetCompaniesWithHttpClientFactory();}
同样,让我们在Program类中注册这个服务:
private static void ConfigureServices(IServiceCollection services) { services.AddHttpClient(); ... services.AddScoped<IHttpClientServiceImplementation, HttpClientFactoryService>(); }
在新方法中放置断点并启动两个应用程序:
使用命名的HttpClient实例
在Program类中,我们使用AddHttpClient方法注册IHttpClientFactory,而不需要额外的配置。这意味着用CreateClient方法创建的每个HttpClient实例都将具有相同的配置。但通常,这是不够的,因为我们的客户端应用程序在与一个或多个api通信时经常需要不同的HttpClient实例。为了支持这一点,我们可以使用命名的HttpClient实例。
在之前的文章中,我们在每个服务中使用了相同的配置来设置基址、超时和清除默认请求头。现在,我们也可以这样做,但只有一个地方:
private static void ConfigureServices(IServiceCollection services) { services.AddHttpClient("CompaniesClient", config => { config.BaseAddress = new Uri("https://localhost:5001/api/"); config.Timeout = new TimeSpan(0, 0, 30); config.DefaultRequestHeaders.Clear(); }); ... services.AddScoped<IHttpClientServiceImplementation, HttpClientFactoryService>(); }
通过这些修改,AddHttpClient方法将IHttpClientFactory添加到服务集合中,并配置一个已命名的HttpClient实例。我们为实例提供一个名称和一个默认配置。在此之后,可以在我们的新服务中修改方法:
private async Task GetCompaniesWithHttpClientFactory() { var httpClient = _httpClientFactory.CreateClient("CompaniesClient"); using (var response = await httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); } }
我们将name参数传递给CreateClient方法,而且不必在GetAsync方法中使用完整的URI。由于使用的是客户机的名称,因此应用与此名称对应的配置。
一旦我们启动这两个应用程序,将得到和之前一样的结果:
使用类型化HttpClient实例
使用类型化实例,我们可以实现与命名实例相同的功能,但在注册过程中不必使用字符串——可以使用类型。首先在客户端应用程序中创建一个新的Clients文件夹,并在该文件夹中创建一个新的CompaniesClient类:
public class CompaniesClient { public HttpClient Client { get; } public CompaniesClient(HttpClient client) { Client = client; Client.BaseAddress = new Uri("https://localhost:5001/api/"); Client.Timeout = new TimeSpan(0, 0, 30); Client.DefaultRequestHeaders.Clear(); } }
这是我们使用默认配置的类型化客户端类,我们可以通过在ConfigureServices方法中再次调用AddHttpClient来在程序类中注册它:
services.AddHttpClient<CompaniesClient>();
因此,我们使用的不是客户端的名称,而是客户端的类型。
现在,在我们的HttpClientFactoryService中,必须注入新的客户端:
private readonly IHttpClientFactory _httpClientFactory; private readonly CompaniesClient _companiesClient; private readonly JsonSerializerOptions _options; public HttpClientFactoryService(IHttpClientFactory httpClientFactory, CompaniesClient companiesClient) { _httpClientFactory = httpClientFactory; _companiesClient = companiesClient; _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; }
然后,我们将创建一个新方法来使用类型化客户端:
private async Task GetCompaniesWithTypedClient() { using (var response = await _companiesClient.Client.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); } }
我们不是通过使用CreateClient方法来创建一个新的客户端实例。这一次,我们只使用注入类型的客户机及其client属性。最后,执行这个方法:
public async Task Execute() { //await GetCompaniesWithHttpClientFactory(); await GetCompaniesWithTypedClient(); }
现在让我们看看如何将相关的逻辑提取到CompaniesClient 类。
封装与类型化客户端相关的逻辑
因为我们已经有了类型化的客户端类,所以我们可以将服务中的所有相关逻辑提取到这个类中。为此,我们将修改CompaniesClient 类:
public class CompaniesClient { private readonly HttpClient _client; private readonly JsonSerializerOptions _options; public CompaniesClient(HttpClient client) { _client = client; _client.BaseAddress = new Uri("https://localhost:5001/api/"); _client.Timeout = new TimeSpan(0, 0, 30); _client.DefaultRequestHeaders.Clear(); _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; } }
我们有一个私有的只读变量,将在该类中使用它来执行HttpClient的逻辑。此外,我们还添加了JsonSerializerOptions配置。现在,可以添加一个新方法:
public async Task<List<CompanyDto>> GetCompanies() { using (var response = await _client.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); return companies; } }
使用这个方法,我们从API中获取公司数据并返回结果。最后,可以修改服务类中的GetCompaniesWithTypedClient方法:
private async Task GetCompaniesWithTypedClient() => await _companiesClient.GetCompanies();
结论
综上所述,在本文中,我们了解到:
- HttpClientFactory解决了哪些问题
- 如何在我们的应用程序中使用HttpClientFactory
- 使用命名和类型化实例的方法
- 如何从服务中提取逻辑到客户端类
原文链接:https://code-maze.com/using-httpclientfactory-in-asp-net-core-applications/