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

版权声明:本文为hhhnicvscs原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/hhhnicvscs/p/14592167.html