Utiliser IHttpClientFactory pour implémenter des requêtes HTTP résilientes

Conseil

Ce contenu est un extrait du livre électronique « .NET Microservices Architecture for Containerized .NET Applications », disponible sur .NET Docs ou sous forme de PDF téléchargeable gratuitement et pouvant être lu hors ligne.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

IHttpClientFactory est un contrat implémenté par DefaultHttpClientFactory, une fabrique avisée, disponible depuis .NET Core 2.1, pour la création d’instances HttpClient à utiliser dans vos applications.

Problèmes liés à la classe HttpClient d’origine disponible dans .NET

La classe HttpClient d’origine et connue peut être facilement utilisée, mais dans certains cas, elle n’est pas correctement utilisée par de nombreux développeurs.

Bien que cette classe implémente IDisposable, la déclaration et l’instanciation dans une instruction using n’est pas préférée, car lorsque l’objet HttpClient est supprimé, le socket sous-jacent n’est pas immédiatement libéré, ce qui peut entraîner un problème d’épuisement du socket . Pour plus d’informations sur ce problème, consultez le billet de blog Vous utilisation de httpClient est incorrecte et déstabilise votre logiciel.

Par conséquent, HttpClient est destiné à être instancié une seule fois et réutilisé tout au long de la durée de vie d’une application. L’instanciation d’une classe HttpClient pour chaque demande épuise le nombre de sockets disponibles sous des charges élevées. Ce problème entraîne des erreurs SocketException. Les approches possibles pour résoudre ce problème sont basées sur la création de l’objet HttpClient singleton ou statique, comme expliqué dans cet article Microsoft sur l’utilisation de HttpClient. Il peut s’agir d’une bonne solution pour les applications console de courte durée ou similaires, qui s’exécutent quelques fois par jour.

Un autre problème que les développeurs rencontrent est l’utilisation d’une instance partagée de HttpClient dans les processus de longue durée. Dans un cas où HttpClient est instancié sous la forme d’un singleton ou d’un objet statique, il ne parvient pas à gérer les modifications DNS décrites dans ce problème du dépôt GitHub dotnet/runtime.

Toutefois, le problème n’est pas vraiment avec HttpClient en soi, mais avec le constructeur par défaut pour HttpClient, car il crée une nouvelle instance concrète de HttpMessageHandler, qui est celle qui a des problèmes d’épuisement des sockets et de modifications DNS mentionnées ci-dessus.

Pour résoudre les problèmes mentionnés ci-dessus et rendre les instances HttpClient gérables, .NET Core 2.1 a introduit deux approches, l’une d’entre elles étant IHttpClientFactory. Il s’agit d’une interface utilisée pour configurer et créer des instances HttpClient dans une application via l’injection de dépendances (DI). Il fournit également des extensions pour l’intergiciel basé sur Polly afin de tirer parti de la délégation de gestionnaires dans HttpClient.

L’alternative consiste à utiliser SocketsHttpHandler avec PooledConnectionLifetime configuré. Cette approche est appliquée aux instances HttpClient à longue durée de vie, static ou uniques. Pour en savoir plus sur les différentes stratégies, consultez Instructions HttpClient pour .NET.

Polly est une bibliothèque de gestion des erreurs temporaires qui permet aux développeurs d’ajouter une résilience à leurs applications, en utilisant des stratégies prédéfinies de manière fluent et thread-safe.

Avantages de l’utilisation de IHttpClientFactory

L’implémentation actuelle de IHttpClientFactory, qui implémente également IHttpMessageHandlerFactory, offre les avantages suivants :

  • Fournit un emplacement central pour nommer et configurer des objets HttpClient logiques. Par exemple, vous pouvez configurer un client (Service Agent) qui est préconfiguré pour accéder à un microservice spécifique.
  • Codifiez le concept de middleware sortant via la délégation de gestionnaires dans HttpClient et l’implémentation du middleware basé sur Polly pour tirer parti des stratégies de Polly pour la résilience.
  • HttpClient intègre déjà le concept de délégation des gestionnaires qui pourraient être liés ensemble pour les requêtes HTTP sortantes. Vous pouvez inscrire des clients HTTP dans la fabrique et utiliser un gestionnaire Polly pour utiliser des stratégies Polly pour réessayer, CircuitBreakers, et ainsi de suite.
  • Gérez la durée de vie de HttpMessageHandler pour éviter les problèmes mentionnés qui peuvent se produire lors de la gestion des durées de vie de HttpClient vous-même.

Conseil

Les instances HttpClient injectées par DI peuvent être supprimées en toute sécurité, car la HttpMessageHandler associée est gérée par la fabrique. Les instances injectées HttpClient sont temporaires du point de vue de l’injection de dépendances, tandis que les instances HttpMessageHandler peuvent être considérées comme étendues. Les instances HttpMessageHandler ont leurs propres étendues d’injection de dépendances, distinctes des étendues d’application (par exemple, étendues de requête entrante ASP.NET). Pour plus d’informations, consultez Utilisation de HttpClientFactory dans .NET.

Notes

L’implémentation de IHttpClientFactory (DefaultHttpClientFactory) est étroitement liée à l’implémentation de l’interface de domaine dans le package NuGet Microsoft.Extensions.DependencyInjection. Si vous avez besoin d’utiliser HttpClient sans injection de dépendances ou avec d’autres implémentations de d’injection de dépendances, envisagez d’utiliser un static ou un HttpClient unique avec la configuration PooledConnectionLifetime. Pour plus d’informations, consultez Instructions HttpClient pour .NET.

Plusieurs façons d’utiliser IHttpClientFactory

Il existe diverses façons d’utiliser IHttpClientFactory dans votre application :

  • Utilisation de base
  • Utiliser des clients nommés
  • Utiliser des clients typés
  • Utiliser des clients générés

Par souci de concision, ces conseils montrent la façon la plus structurée d’utiliser IHttpClientFactory, qui consiste à utiliser des clients typés (modèle Agent de service). Toutefois, toutes les options sont documentées et sont actuellement répertoriées dans cet article couvrant l’IHttpClientFactoryutilisation.

Notes

Si votre application nécessite des cookies, il peut être préférable d’éviter d’utiliser IHttpClientFactory dans votre application. Pour obtenir d’autres méthodes de gestion des clients, consultez Recommandations pour l’utilisation des clients HTTP

Comment utiliser des clients typés avec IHttpClientFactory

Mais qu’est-ce donc qu’un « client typé » ? Il s’agit simplement d’un HttpClient préconfiguré pour une utilisation spécifique. Cette configuration peut inclure des valeurs spécifiques telles que le serveur de base, les en-têtes HTTP ou les délais d’attente.

Le diagramme suivant montre comment les clients typés sont utilisés avec IHttpClientFactory :

Diagram showing how typed clients are used with IHttpClientFactory.

Figure 8-4. Utilisation de IHttpClientFactory avec des classes client typées.

Dans l’image ci-dessus, un ClientService (utilisé par un contrôleur ou un code client) utilise un HttpClient créé par le IHttpClientFactoryinscrit. Cette fabrique affecte une HttpMessageHandler d’un pool au HttpClient. Le HttpClient peut être configuré avec les stratégies de Polly lors de l’inscription du IHttpClientFactory dans le conteneur DI avec la méthode d’extension AddHttpClient.

Pour configurer la structure ci-dessus, ajoutez IHttpClientFactory dans votre application en installant le package NuGet Microsoft.Extensions.Http qui inclut la AddHttpClient méthode d’extension pour IServiceCollection. Cette méthode d’extension inscrit la classe de DefaultHttpClientFactory interne à utiliser comme singleton pour l’interface IHttpClientFactory. Elle définit une configuration temporaire pour HttpMessageHandlerBuilder. Ce gestionnaire de messages (objet HttpMessageHandler), obtenu à partir d’un pool, est utilisé par le HttpClient retourné à partir de la fabrique.

Dans l’extrait suivant, vous pouvez voir comment utiliser AddHttpClient() pour enregistrer les clients typés (agents de service) qui ont besoin d’utiliser HttpClient.

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

L’inscription des services clients comme indiqué dans l’extrait précédent permet aux DefaultClientFactory de créer un HttpClient standard pour chaque service. Le client typé est inscrit en tant que temporaire avec le conteneur d’adresses DI. Dans le code précédent, AddHttpClient() enregistreCatalogService, BasketService, OrderingService en tant que services temporaires afin qu’ils puissent être injectés et consommés directement sans avoir besoin d’inscriptions supplémentaires.

Vous pouvez également ajouter une configuration spécifique à l’instance dans l’inscription pour, par exemple, configurer l’adresse de base et ajouter des stratégies de résilience, comme indiqué dans l’exemple suivant :

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

Dans l’exemple suivant, vous pouvez voir la configuration de l’une des stratégies ci-dessus :

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

Vous trouverez plus d’informations sur l’utilisation de Polly dans l’article Suivant.

Durées de vie de HttpClient

Chaque fois que vous obtenez un objet HttpClient à partir de IHttpClientFactory, une nouvelle instance est retournée. Mais chaque HttpClient utilise un HttpMessageHandler qui est mis en pool et réutilisé par IHttpClientFactory pour réduire la consommation de ressources, tant que la durée de vie du HttpMessageHandler n’a pas expiré.

Le regroupement de gestionnaires est souhaitable, car chaque gestionnaire gère généralement ses propres connexions HTTP sous-jacentes : créer plus de gestionnaires que nécessaire peut entraîner des délais dans la connexion. Certains gestionnaires conservent aussi les connexions indéfiniment ouvertes, ce qui peut empêcher le gestionnaire de réagir aux changements du DNS.

Les objets HttpMessageHandler du pool ont une durée de vie qui correspond à la durée pendant laquelle une instance HttpMessageHandler du pool peut être réutilisée. La valeur par défaut est de deux minutes, mais elle peut être remplacée pour chaque client typé. Pour la remplacer, appelez SetHandlerLifetime() sur le IHttpClientBuilder qui est retourné lors de la création du client, comme indiqué dans le code suivant :

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

Chaque client typé peut avoir sa propre valeur de durée de vie de gestionnaire configurée. Définissez la durée de vie sur InfiniteTimeSpan pour désactiver l’expiration du gestionnaire.

Implémenter les classes de client typé qui utilisent l’objet HttpClient injecté et configuré

À l’étape précédente, vous devez définir vos classes clientes typées, telles que les classes dans l’exemple de code, telles que « BasketService », « CatalogService », « OrderingService », etc. – Un client typé est une classe qui accepte un objet HttpClient (injecté via son constructeur) et l’utilise pour appeler un service HTTP distant. Par exemple :

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;
    }
}

Le client typé (CatalogService dans l’exemple) est activé par DI (Injection de dépendances), ce qui signifie qu’il peut accepter n’importe quel service inscrit dans son constructeur, en plus de HttpClient.

Un client typé est effectivement un objet temporaire, ce qui signifie qu’une nouvelle instance est créée chaque fois qu’une instance est nécessaire. Il reçoit une nouvelle instance HttpClient chaque fois qu’elle est construite. Toutefois, les objets HttpMessageHandler dans le pool sont les objets réutilisés par plusieurs instances HttpClient .

Utiliser les classes de client typé

Enfin, une fois que vos classes typées ont été implémentées, vous pouvez les inscrire et les configurer avec AddHttpClient(). Après cela, vous pouvez les utiliser partout où les services sont injectés par injection de dépendances, par exemple dans le code de page Razor ou un contrôleur d’application web MVC, illustré dans le code ci-dessous d’eShopOnContainers :

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
        }

        }
}

Jusqu’à ce stade, l’extrait de code ci-dessus ne montre que l’exemple d’exécution de requêtes HTTP régulières. Mais la « magie » se trouve dans les sections suivantes où il montre comment toutes les requêtes HTTP effectuées par HttpClient, peuvent avoir des stratégies résilientes telles que des nouvelles tentatives avec des interruptions exponentielles, des disjoncteurs, des fonctionnalités de sécurité à l’aide de jetons d’authentification, ou même toute autre fonctionnalité personnalisée. Et toutes ces opérations peuvent être effectuées simplement en ajoutant des stratégies et en déléguant des gestionnaires à vos clients typés inscrits.

Ressources supplémentaires