到目前為止,我們一直直接使用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/