HttpClientFactory in .NetCore —— 最终的解决方案

为每个请求创建 HttpClient 以避免并发问题,但仍可有效使用background socket connection 。

 services.AddHttpClient

 

Use IHttpClientFactory to implement resilient HTTP requests | Microsoft Docs

 

书籍推荐:.NET Microservices Architecture for Containerized .NET Applications;

Download Microservices architecture e-book (PDF) (microsoft.com)

 

Issues with the original HttpClient class available in .NET

1. The original and well-known HttpClient class can be easily used, but in some cases, it isn't being properly used by many developers.

Though this class implements , declaring and instantiating it within a statement is not preferred because when the object gets disposed of, the underlying socket is not immediately released, which can lead tosocket exhaustion problem. For more information about this issue, see the blog post You're using HttpClient wrong and it's destabilizing your software.IDisposableusingHttpClient

 

 2. Therefore, is intended to be instantiated once and reused throughout the life of an application. Instantiating an class for every request will exhaust the number of sockets available under heavy loads. That issue will result in errors. Possible approaches to solve that problem are based on the creation of the object as singleton or static, as explained in this Microsoft article on HttpClient usage. This can be a good solution for short-lived console apps or similar, that run a few times a day.HttpClientHttpClientSocketExceptionHttpClient

 

3. Another issue that developers run into is when using a shared instance of HttpClient in long-running processes. In a situation where the HttpClient is instantiated as a singleton or a static object, it fails to handle the DNS changes as described in this issue of the dotnet/runtime GitHub repository.

However, the issue isn't really with HttpClient per se, but with the default constructor for HttpClient, because it creates a new concrete instance of HttpMessageHandler, which is the one that has sockets exhaustion and DNS changes issues mentioned above.

 

To address the issues mentioned above and to make HttpClient instances manageable, .NET Core 2.1 introduced the IHttpClientFactory interface which can be used to configure and create HttpClient instances in an app through Dependency Injection (DI). It also provides extensions for Polly-based middleware to take advantage of delegating handlers in HttpClient.

Polly is a transient-fault-handling library that helps developers add resiliency to their applications, by using some pre-defined policies in a fluent and thread-safe manner.

 

 

Benefits of using IHttpClientFactory

The current implementation of IHttpClientFactory, that also implements IHttpMessageHandlerFactory, offers the following benefits:

  • Provides a central location for naming and configuring logical HttpClient objects. For example, you may configure a client (Service Agent) that's pre-configured to access a specific microservice.
  • Codify the concept of outgoing middleware via delegating handlers in HttpClient and implementing Polly-based middleware to take advantage of Polly's policies for resiliency.
  • HttpClient already has the concept of delegating handlers that could be linked together for outgoing HTTP requests. You can register HTTP clients into the factory and you can use a Polly handler to use Polly policies for Retry, CircuitBreakers, and so on.
  • Manage the lifetime of HttpMessageHandler to avoid the mentioned problems/issues that can occur when managing HttpClient lifetimes yourself.

The instances injected by DI, can be disposed of safely, because the associated is managed by the factory. As a matter of fact, injected instances are Scoped from a DI perspective.HttpClientHttpMessageHandlerHttpClient

 

The implementation of () is tightly tied to the DI implementation in the NuGet package. For more information about using other DI containers, see this GitHub discussion.IHttpClientFactoryDefaultHttpClientFactoryMicrosoft.Extensions.DependencyInjection

 

Multiple ways to use IHttpClientFactory

There are several ways that you can use in your application:IHttpClientFactory

  • Basic usage
  • Use Named Clients
  • Use Typed Clients
  • Use Generated Clients

For the sake of brevity, this guidance shows the most structured way to use , which is to use Typed Clients (Service Agent pattern). However, all options are documented and are currently listed in this article covering the IHttpClientFactory usage.IHttpClientFactory

 

How to use Typed Clients with IHttpClientFactory

So, what's a "Typed Client"? It's just an that's pre-configured for some specific use. This configuration can include specific values such as the base server, HTTP headers or time outs.HttpClient

The following diagram shows how Typed Clients are used with :IHttpClientFactory

 

 

In the above image, a (used by a controller or client code) uses an created by the registered . This factory assigns an from a pool to the . The can be configured with Polly's policies when registering the in the DI container with the extension method AddHttpClient.ClientService HttpClient IHttpClientFactory HttpMessageHandler HttpClientHttpClient IHttpClientFactory

To configure the above structure, add IHttpClientFactory in your application by installing the NuGet package that includes the AddHttpClient extension method for IServiceCollection. This extension method registers the internal class to be used as a singleton for the interface . It defines a transient configuration for the HttpMessageHandlerBuilder. This message handler (HttpMessageHandler object), taken from a pool, is used by the returned from the factory.Microsoft.Extensions.HttpDefaultHttpClientFactoryIHttpClientFactoryHttpClient

how can be used to register Typed Clients (Service Agents) that need to use .AddHttpClient()HttpClient

// Startup.cs
//Add http client services at ConfigureServices(IServiceCollection services)
services.AddHttpClient<ICatalogService, CatalogService>();
services.AddHttpClient<IBasketService, BasketService>();
services.AddHttpClient<IOrderingService, OrderingService>();

  Registering the client services as shown in the previous code, makes the create a standard for each service. The typed client is registered as transient with DI container. In the preceding code, registers CatalogServiceBasketServiceOrderingService as transient services so they can be injected and consumed directly without any need for additional registrations.DefaultClientFactoryHttpClientAddHttpClient()

 

add instance-specific configuration in the registration to, for example, configure the base address, and add some resiliency policies, as shown in the following code:

services.AddHttpClient<ICatalogService, CatalogService>(client =>
{
    client.BaseAddress = new Uri(Configuration["BaseUrl"]);
})
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy());

  

 

Just for the example sake, you can see one of the above policies in the next code:

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
        .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}

  You can find more details about using Polly in the Next article.

 

HttpClient lifetimes

Each time you get an object from the , a new instance is returned. But each uses an that's pooled and reused by the to reduce resource consumption, as long as the 's lifetime hasn't expired.HttpClientIHttpClientFactoryHttpClientHttpMessageHandlerIHttpClientFactoryHttpMessageHandler

Pooling of handlers is desirable as each handler typically manages its own underlying HTTP connections; creating more handlers than necessary can result in connection delays. Some handlers also keep connections open indefinitely, which can prevent the handler from reacting to DNS changes.

 

 The objects in the pool have a lifetime that's the length of time that an instance in the pool can be reused. The default value is two minutes, but it can be overridden per Typed Client. To override it, call on the IHttpClientBuilder that's returned when creating the client, as shown in the following code:HttpMessageHandlerHttpMessageHandlerSetHandlerLifetime()

//Set 5 min as the lifetime for the HttpMessageHandler objects in the pool used for the Catalog Typed Client
services.AddHttpClient<ICatalogService, CatalogService>()
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

  Each Typed Client can have its own configured handler lifetime value. Set the lifetime to to disable handler expiry.InfiniteTimeSpan

 

Implement your Typed Client classes that use the injected and configured HttpClient

As a previous step, you need to have your Typed Client classes defined, such as the classes in the sample code, like 'BasketService', 'CatalogService', 'OrderingService', etc. – A Typed Client is a class that accepts an object (injected through its constructor) and uses it to call some remote HTTP service. For example:HttpClient

public class CatalogService : ICatalogService
{
    private readonly HttpClient _httpClient;
    private readonly string _remoteServiceBaseUrl;

    public CatalogService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Catalog> GetCatalogItems(int page, int take,
                                               int? brand, int? type)
    {
        var uri = API.Catalog.GetAllCatalogItems(_remoteServiceBaseUrl,
                                                 page, take, brand, type);

        var responseString = await _httpClient.GetStringAsync(uri);

        var catalog = JsonConvert.DeserializeObject<Catalog>(responseString);
        return catalog;
    }

 

A Typed Client is effectively a transient object, that means a new instance is created each time one is needed. It receives a new instance each time it's constructed. However, the objects in the pool are the objects that are reused by multiple instances.HttpClient HttpMessageHandler HttpClient

 

Use your Typed Client classes

Finally, once you have your typed classes implemented, you can have them registered and configured with .

After that you can use them wherever have services injected by DI.

For example, in a Razor page code or controller of an MVC web app, like in the following code from eShopOnContainers:AddHttpClient()

namespace Microsoft.eShopOnContainers.WebMVC.Controllers
{
    public class CatalogController : Controller
    {
        private ICatalogService _catalogSvc;

        public CatalogController(ICatalogService catalogSvc) =>
                                                           _catalogSvc = catalogSvc;

        public async Task<IActionResult> Index(int? BrandFilterApplied,
                                               int? TypesFilterApplied,
                                               int? page,
                                               [FromQuery]string errorMsg)
        {
            var itemsPage = 10;
            var catalog = await _catalogSvc.GetCatalogItems(page ?? 0,
                                                            itemsPage,
                                                            BrandFilterApplied,
                                                            TypesFilterApplied);
            //… Additional code
        }

        }
}

  

Up to this point, the above code snippet has only shown the example of performing regular HTTP requests. But the 'magic' comes in the following sections where it shows how all the HTTP requests made by , can have resilient policies such as retries with exponential backoff, circuit breakers, security features using auth tokens, or even any other custom feature. And all of these can be done just by adding policies and delegating handlers to your registered Typed Clients.HttpClient

 

 

Additional resources

posted @ 2022-07-11 17:10  PanPan003  阅读(106)  评论(0编辑  收藏  举报