在ASP.NET Core中用HttpClient(六)——ASP.NET Core中使用HttpClientFactory

碼農譯站發表於2021-03-29

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

相關文章