Effectuer des requêtes HTTP en utilisant IHttpClientFactory dans ASP.NET Core

Par Kirk Larkin, Steve Gordon, Glenn Condron et Ryan Nowak.

Une IHttpClientFactory peut être inscrite et utilisée pour configurer et créer des instances de HttpClient dans une application. IHttpClientFactory offre les avantages suivants :

  • Fournit un emplacement central pour le nommage et la configuration d’instance de HttpClient logiques. Par exemple, un client nommé github peut être inscrit et configuré pour accéder à GitHub. Un client par défaut peut être inscrit pour l’accès général.
  • Codifie le concept d’intergiciel (middleware) sortant via la délégation de gestionnaires dans HttpClient. Fournit des extensions pour les intergiciels basés sur Polly afin de tirer parti de la délégation de gestionnaires dans HttpClient.
  • Gère les pools et la durée de vie des instances HttpClientMessageHandler sous-jacentes. La gestion automatique évite les problèmes courants liés au système DNS (Domain Name System) qui se produisent lors de la gestion manuelle des durées de vie HttpClient.
  • Ajoute une expérience de journalisation configurable (via ILogger) pour toutes les requêtes envoyées via des clients créés par la fabrique.

L’exemple de code de cette version de rubrique utilise System.Text.Json pour désérialiser le contenu JSON retourné dans les réponses HTTP. Pour les exemples qui utilisent Json.NET et ReadAsAsync<T>, utilisez le sélecteur de version pour sélectionner une version 2.x de cette rubrique.

Modèles de consommation

Vous pouvez utiliser IHttpClientFactory dans une application de plusieurs façons :

La meilleure approche dépend des exigences de l’application.

Utilisation de base

Inscrivez IHttpClientFactory en appelant AddHttpClient dans Program.cs :

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddHttpClient();

Une IHttpClientFactory peut être demandée à l’aide de l’injection de dépendances (DI). Le code suivant utilise IHttpClientFactory pour créer une instance HttpClient :

public class BasicModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public BasicModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { HeaderNames.Accept, "application/vnd.github.v3+json" },
                { HeaderNames.UserAgent, "HttpRequestsSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}

L’utilisation de IHttpClientFactory comme dans l’exemple précédent est un bon moyen de refactoriser une application existante. Cela n’a aucun impact sur la façon dont HttpClient est utilisé. Aux endroits où des instances HttpClient sont créées dans une application existante, remplacez ces occurrences par des appels à CreateClient.

Clients nommés

Les clients nommés sont un bon choix dans les cas suivants :

  • L’application nécessite de nombreuses utilisations distinctes de HttpClient.
  • De nombreuses instances HttpClient ont une configuration différente.

Spécifiez la configuration d’un HttpClient nommé pendant son inscription dans Program.cs :

builder.Services.AddHttpClient("GitHub", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // using Microsoft.Net.Http.Headers;
    // The GitHub API requires two headers.
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.Accept, "application/vnd.github.v3+json");
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.UserAgent, "HttpRequestsSample");
});

Dans le code précédent, le client est configuré avec :

  • L’adresse de base https://api.github.com/.
  • Deux en-têtes requis pour fonctionner avec l’API GitHub.

CreateClient

Chaque fois que CreateClient est appelé :

  • Une nouvelle instance de HttpClient est créée.
  • L’action de configuration est appelée.

Pour créer un client nommé, passez son nom dans CreateClient :

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public NamedClientModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpClient = _httpClientFactory.CreateClient("GitHub");
        var httpResponseMessage = await httpClient.GetAsync(
            "repos/dotnet/AspNetCore.Docs/branches");

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}

Dans le code précédent, la requête n’a pas besoin de spécifier un nom d’hôte. Le code peut simplement passer le chemin, car l’adresse de base configurée pour le client est utilisée.

Clients typés

Clients typés :

  • Fournissent les mêmes fonctionnalités que les clients nommés, sans qu’il soit nécessaire d’utiliser des chaînes comme clés.
  • Bénéficie de l’aide d’IntelliSense et du compilateur lors de l’utilisation des clients.
  • Fournissent un emplacement unique pour configurer et interagir avec un HttpClient particulier. Par exemple, un client typé unique peut être utilisé :
    • Pour un point de terminaison principal unique.
    • Pour encapsuler toute la logique traitant du point de terminaison.
  • Pour travailler avec l’injection de dépendances et injecter là où c’est nécessaire dans l’application.

Un client typé accepte un paramètre HttpClient dans son constructeur :

public class GitHubService
{
    private readonly HttpClient _httpClient;

    public GitHubService(HttpClient httpClient)
    {
        _httpClient = httpClient;

        _httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    }

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync() =>
        await _httpClient.GetFromJsonAsync<IEnumerable<GitHubBranch>>(
            "repos/dotnet/AspNetCore.Docs/branches");
}

Dans le code précédent :

  • La configuration est déplacée dans le client typé.
  • L’instance HttpClient fournie est stockée en tant que champ privé.

Vous pouvez créer des méthodes spécifiques à l’API qui exposent des fonctionnalités de HttpClient. Par exemple, la méthode encapsule le code GetAspNetCoreDocsBranches pour récupérer des branches GitHub de documentation.

Le code suivant appelle AddHttpClient dans Program.cs pour inscrire une classe cliente typée GitHubService :

builder.Services.AddHttpClient<GitHubService>();

Le client typé est inscrit comme étant transitoire avec injection de dépendances. Dans le code précédent, AddHttpClient inscrit GitHubService en tant que service temporaire. Cette inscription utilise une méthode de fabrique pour :

  1. Créez une instance de HttpClient.
  2. Créer une instance de GitHubService en passant l’instance de HttpClient à son constructeur.

Le client typé peut être injecté et utilisé directement :

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public TypedClientModel(GitHubService gitHubService) =>
        _gitHubService = gitHubService;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubService.GetAspNetCoreDocsBranchesAsync();
        }
        catch (HttpRequestException)
        {
            // ...
        }
    }
}

Vous pouvez également spécifier la configuration d’un client typé lors de l’inscription dans Program.cs au lieu de le faire dans le constructeur du client typé :

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // ...
});

Clients générés

IHttpClientFactory peut être utilisé en combinaison avec des bibliothèques tierces, comme Refit. Refit est une bibliothèque REST pour .NET. Il convertit les API REST en interfaces dynamiques. Appelez AddRefitClient pour générer une implémentation dynamique d’une interface, qui utilise HttpClient pour effectuer les appels HTTP externes.

Une interface personnalisée représente l’API externe :

public interface IGitHubClient
{
    [Get("/repos/dotnet/AspNetCore.Docs/branches")]
    Task<IEnumerable<GitHubBranch>> GetAspNetCoreDocsBranchesAsync();
}

Appelez AddRefitClient pour générer l’implémentation dynamique, puis appelez ConfigureHttpClient pour configurer le HttpClient sous-jacent :

builder.Services.AddRefitClient<IGitHubClient>()
    .ConfigureHttpClient(httpClient =>
    {
        httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    });

Utilisez la DI pour accéder à l’implémentation dynamique de IGitHubClient :

public class RefitModel : PageModel
{
    private readonly IGitHubClient _gitHubClient;

    public RefitModel(IGitHubClient gitHubClient) =>
        _gitHubClient = gitHubClient;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubClient.GetAspNetCoreDocsBranchesAsync();
        }
        catch (ApiException)
        {
            // ...
        }
    }
}

Effectuer des requêtes POST, PUT et DELETE

Dans les exemples précédents, toutes les requêtes HTTP utilisent le verbe HTTP GET. HttpClient prend également en charge d’autres verbes HTTP, notamment :

  • POST
  • PUT
  • DELETE
  • PATCH

Pour obtenir la liste complète des verbes HTTP pris en charge, consultez HttpMethod.

L’exemple suivant montre comment effectuer une requête HTTP POST :

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json); // using static System.Net.Mime.MediaTypeNames;

    using var httpResponseMessage =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponseMessage.EnsureSuccessStatusCode();
}

Dans le code précédent, la méthode CreateItemAsync :

  • Sérialise le paramètre TodoItem au format JSON à l’aide de System.Text.Json.
  • Crée une instance de StringContent pour empaqueter le JSON sérialisé pour l’envoi dans le corps de la requête HTTP.
  • Appelle PostAsync pour envoyer le contenu JSON à l’URL spécifiée. Il s’agit d’une URL relative qui est ajoutée à HttpClient.BaseAddress.
  • Appelle EnsureSuccessStatusCode pour lever une exception si le code d’état de la réponse n’indique pas la réussite.

HttpClient prend également en charge d’autres types de contenu. Par exemple : MultipartContent et StreamContent. Pour obtenir la liste complète du contenu pris en charge, consultez HttpContent.

L’exemple suivant montre une requête HTTP PUT :

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json);

    using var httpResponseMessage =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponseMessage.EnsureSuccessStatusCode();
}

Le code précédent est similaire à l’exemple POST La méthode SaveItemAsync appelle PutAsync au lieu de PostAsync.

L’exemple suivant montre une requête HTTP DELETE :

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponseMessage =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponseMessage.EnsureSuccessStatusCode();
}

Dans le code précédent, la méthode DeleteItemAsync appelle DeleteAsync. Étant donné que les requêtes HTTP DELETE ne contiennent généralement aucun corps, la méthode DeleteAsync ne fournit pas de surcharge qui accepte une instance de HttpContent.

Pour en savoir plus sur l’utilisation de différents verbes HTTP avec HttpClient, consultez HttpClient.

Middleware pour les requêtes sortantes

HttpClient intègre le concept de délégation des gestionnaires qui peuvent être liés ensemble pour les requêtes HTTP sortantes. IHttpClientFactory:

  • Simplifie la définition des gestionnaires à appliquer pour chaque client nommé.
  • Il prend en charge l’inscription et le chaînage de plusieurs gestionnaires pour créer un pipeline d’intergiciels pour les requêtes sortantes. Chacun de ces gestionnaires peut effectuer un travail avant et après la requête sortante. Ce modèle :
    • Est similaire au pipeline d’intergiciels entrants dans ASP.NET Core.
    • Fournit un mécanisme pour gérer les problèmes transversaux liés aux requêtes HTTP, par exemple :
      • mise en cache
      • gestion des erreurs
      • sérialisation
      • journalisation

Pour créer un gestionnaire de délégation :

  • Dérivez de DelegatingHandler.
  • Remplacez SendAsync. Exécutez du code avant de passer la requête au gestionnaire suivant dans le pipeline :
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "The API key header X-API-KEY is required.")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Le code précédent vérifie si l’en-tête X-API-KEY se trouve dans la requête. Si X-API-KEY est manquant, BadRequest est retourné.

Plusieurs gestionnaires peuvent être ajoutés à la configuration d’un HttpClient avec Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler :

builder.Services.AddTransient<ValidateHeaderHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
    .AddHttpMessageHandler<ValidateHeaderHandler>();

Dans le code précédent, le ValidateHeaderHandler est inscrit avec une injection de dépendances. Une fois inscrit, AddHttpMessageHandler peut être appelé en passant en entrée le type pour le gestionnaire.

Vous pouvez inscrire plusieurs gestionnaires dans l’ordre où ils doivent être exécutés. Chaque gestionnaire wrappe le gestionnaire suivant jusqu’à ce que le dernier HttpClientHandler exécute la requête :

builder.Services.AddTransient<SampleHandler1>();
builder.Services.AddTransient<SampleHandler2>();

builder.Services.AddHttpClient("MultipleHttpMessageHandlers")
    .AddHttpMessageHandler<SampleHandler1>()
    .AddHttpMessageHandler<SampleHandler2>();

Dans le code précédent, SampleHandler1 s’exécute en premier, avant SampleHandler2.

Utiliser l’intergiciel de requêtes sortantes

Lorsque IHttpClientFactory crée un gestionnaire de délégation, il utilise l’injection de dépendances pour remplir les paramètres du constructeur du gestionnaire. IHttpClientFactory crée une étendue de DI distincte pour chaque gestionnaire, ce qui peut entraîner un comportement surprenant lorsqu’un gestionnaire consomme un service délimité.

Par exemple, considérez l’interface suivante et son implémentation, qui représente une tâche en tant qu’opération avec un identificateur, OperationId :

public interface IOperationScoped
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Comme son nom l’indique, IOperationScoped est inscrit avec la DI à l’aide d’une durée de vie étendue :

builder.Services.AddScoped<IOperationScoped, OperationScoped>();

Le gestionnaire de délégation suivant consomme et utilise IOperationScoped pour définir l’en-tête X-OPERATION-ID pour la requête sortante :

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationScoped;

    public OperationHandler(IOperationScoped operationScoped) =>
        _operationScoped = operationScoped;

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationScoped.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

Dans le Téléchargement de HttpRequestsSample, accédez à /Operation et actualisez la page. La valeur de l’étendue de la requête change pour chaque requête, mais la valeur d’étendue du gestionnaire change uniquement toutes les 5 secondes.

Les gestionnaires peuvent dépendre des services de n’importe quelle étendue. Les services dont dépendent les gestionnaires sont supprimés lorsque le gestionnaire est supprimé.

Utilisez l’une des approches suivantes pour partager l’état de chaque requête avec les gestionnaires de messages :

Utiliser les gestionnaires Polly

IHttpClientFactory s’intègre à la bibliothèque tierce Polly. Polly est une bibliothèque complète de gestion des erreurs transitoires et de résilience pour .NET. Elle permet aux développeurs de formuler facilement et de façon thread-safe des stratégies, comme Retry (Nouvelle tentative), Circuit Breaker (Disjoncteur), Timeout (Délai d’attente), Bulkhead Isolation (Isolation par cloisonnement) et Fallback (Alternative de repli).

Des méthodes d’extension sont fournies pour permettre l’utilisation de stratégies Polly avec les instances configurées de HttpClient. Les extensions Polly prennent en charge l’ajout de gestionnaires basés sur Polly aux clients. Polly nécessite le package NuGet Microsoft.Extensions.Http.Polly.

Gérer les erreurs temporaires

Les erreurs se produisent généralement lorsque des appels HTTP externes sont temporaires. AddTransientHttpErrorPolicy permet à une stratégie d’être définie pour gérer les erreurs temporaires. Les stratégies configurées avec AddTransientHttpErrorPolicy gèrent les réponses suivantes :

AddTransientHttpErrorPolicy fournit l’accès à un objet PolicyBuilder configuré pour gérer les erreurs représentant une erreur temporaire possible :

builder.Services.AddHttpClient("PollyWaitAndRetry")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.WaitAndRetryAsync(
            3, retryNumber => TimeSpan.FromMilliseconds(600)));

Dans le code précédent, une stratégie WaitAndRetryAsync est définie. Les requêtes qui ont échoué sont retentées jusqu’à trois fois avec un délai de 600 ms entre les tentatives.

Sélectionner dynamiquement des stratégies

Des méthodes d’extension sont fournies pour ajouter des gestionnaires basés sur Polly, par exemple AddPolicyHandler. La surcharge de AddPolicyHandler suivante inspecte la requête pour déterminer la stratégie à appliquer :

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

builder.Services.AddHttpClient("PollyDynamic")
    .AddPolicyHandler(httpRequestMessage =>
        httpRequestMessage.Method == HttpMethod.Get ? timeoutPolicy : longTimeoutPolicy);

Dans le code précédent, si la requête sortante est un HTTP GET, un délai d’attente de 10 secondes est appliqué. Pour toutes les autres méthodes HTTP, un délai d’attente de 30 secondes est utilisé.

Ajouter plusieurs gestionnaires Polly

Il est courant d’imbriquer les stratégies Polly :

builder.Services.AddHttpClient("PollyMultiple")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.RetryAsync(3))
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

Dans l’exemple précédent :

  • Deux gestionnaires sont ajoutés.
  • Le premier gestionnaire utilise AddTransientHttpErrorPolicy pour ajouter une stratégie de nouvelle tentative. Les requêtes qui ont échoué sont retentées jusqu’à trois fois.
  • Le deuxième appel à AddTransientHttpErrorPolicy ajoute une stratégie de disjoncteur. Les requêtes externes supplémentaires sont bloquées pendant 30 secondes si 5 tentatives successives échouent. Les stratégies de disjoncteur sont avec état. Tous les appels effectués via ce client partagent le même état du circuit.

Ajouter des stratégies à partir du Registre Polly

Une approche de la gestion des stratégies régulièrement utilisées consiste à les définir une seule fois et à les inscrire avec un PolicyRegistry. Par exemple :

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

var policyRegistry = builder.Services.AddPolicyRegistry();

policyRegistry.Add("Regular", timeoutPolicy);
policyRegistry.Add("Long", longTimeoutPolicy);

builder.Services.AddHttpClient("PollyRegistryRegular")
    .AddPolicyHandlerFromRegistry("Regular");

builder.Services.AddHttpClient("PollyRegistryLong")
    .AddPolicyHandlerFromRegistry("Long");

Dans le code précédent :

  • Deux stratégies, Regular et Long, sont ajoutées au registre Polly.
  • AddPolicyHandlerFromRegistry configure des clients nommés individuels pour utiliser ces stratégies à partir du registre Polly.

Pour plus d’informations sur les intégrations IHttpClientFactory et Polly, consultez le Wiki Polly.

HttpClient et gestion de la durée de vie

Une nouvelle instance HttpClient est retournée à chaque fois que CreateClient est appelé sur IHttpClientFactory. Un HttpMessageHandler est créé par client nommé. La fabrique gère les durées de vie des instances HttpMessageHandler.

IHttpClientFactory regroupe dans un pool les instances de HttpMessageHandler créées par la fabrique pour réduire la consommation des ressources. Une instance de HttpMessageHandler peut être réutilisée à partir du pool quand vous créez une instance de HttpClient si sa durée de vie n’a pas expiré.

Le regroupement de gestionnaires en pools est souhaitable, car chaque gestionnaire gère habituellement ses propres connexions HTTP sous-jacentes. La création de plus de gestionnaires que nécessaire peut entraîner des délais de connexion. Certains gestionnaires conservent aussi les connexions indéfiniment ouvertes, ce qui peut empêcher le gestionnaire de réagir aux changements du DNS (Domain Name System).

La durée de vie par défaut d’un gestionnaire est de deux minutes. La valeur par défaut peut être remplacée pour chaque client nommé :

builder.Services.AddHttpClient("HandlerLifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Les instances HttpClient peuvent généralement être traitées en tant qu’objets .NET ne nécessitant pas une suppression. La suppression annule les requêtes sortantes et garantit que l’instance HttpClient donnée ne peut pas être utilisée après avoir appelé Dispose. IHttpClientFactory effectue le suivi et libère les ressources utilisées par les instances HttpClient.

Le fait de conserver une seule instance HttpClient active pendant une longue durée est un modèle commun utilisé avant le lancement de IHttpClientFactory. Ce modèle devient inutile après la migration vers IHttpClientFactory.

Alternatives à IHttpClientFactory

L’utilisation de IHttpClientFactory dans une application avec injection de dépendances évite :

  • Les problèmes d’épuisement des ressources en regroupant les instances HttpMessageHandler.
  • Les problèmes de DNS obsolète en permutant les instances HttpMessageHandler à intervalles réguliers.

Il existe d’autres façons de résoudre les problèmes précédents à l’aide d’une instance SocketsHttpHandler de longue durée.

  • Créez une instance de SocketsHttpHandler lorsque l’application démarre et utilisez-la pour la durée de vie de l’application.
  • Configurez PooledConnectionLifetime sur une valeur appropriée en fonction des temps d’actualisation DNS.
  • Créez des instances HttpClient à l’aide de new HttpClient(handler, disposeHandler: false) si nécessaire.

Les approches précédentes résolvent les problèmes de gestion des ressources que IHttpClientFactory résout de la même manière.

  • Le SocketsHttpHandler partage les connexions entre les instances HttpClient. Ce partage empêche l’épuisement des sockets.
  • Le SocketsHttpHandler permute les connexions en fonction de PooledConnectionLifetime pour éviter les problèmes de DNS obsolète.

Journalisation

Les clients créés via IHttpClientFactory enregistrent les messages de journalisation pour toutes les requêtes. Activez le niveau d’informations approprié dans la configuration de journalisation pour voir les messages de journalisation par défaut. Une journalisation supplémentaire, comme celle des en-têtes des requêtes, est incluse seulement au niveau de trace.

La catégorie de journal utilisée pour chaque client comprend le nom du client. Par exemple, un client nommé MyNamedClient journalise les messages avec la catégorie « System.Net.Http.HttpClient.MyNamedClient.LogicalHandler ». Les messages avec le suffixe LogicalHandler se produisent en dehors du pipeline du gestionnaire de requêtes. Lors d’une requête, les messages sont journalisés avant que d’autres gestionnaires du pipeline l’aient traitée. Lors d’une réponse, les messages sont journalisés une fois que tous les autres gestionnaires du pipeline ont reçu la réponse.

La journalisation se produit également à l’intérieur du pipeline du gestionnaire de requêtes. Dans l’exemple MyNamedClient, ces messages sont enregistrés avec la catégorie de journal « System.Net.Http.HttpClient.MyNamedClient.ClientHandler ». Pour la requête, cela se produit après que tous les autres gestionnaires ont été exécutés et immédiatement avant l’envoi de la requête. Lors de la réponse, cette journalisation inclut l’état de la réponse avant qu’elle repasse à travers le pipeline de gestionnaires.

L’activation de la journalisation à l’extérieur et à l’intérieur du pipeline permet l’inspection des changements apportés par les autres gestionnaires du pipeline. Cela peut comprendre des changements apportés aux en-têtes des requêtes ou au code d’état de la réponse.

L’ajout du nom du client dans la catégorie de journalisation permet de filtrer le journal pour des clients nommés spécifiques.

Configurer le HttpMessageHandler

Il peut être nécessaire de contrôler la configuration du HttpMessageHandler interne utilisé par un client.

Un IHttpClientBuilder est retourné quand vous ajoutez des clients nommés ou typés. La méthode d’extension ConfigurePrimaryHttpMessageHandler peut être utilisée pour définir un délégué. Le délégué est utilisé pour créer et configurer le HttpMessageHandler principal utilisé par ce client :

builder.Services.AddHttpClient("ConfiguredHttpMessageHandler")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            AllowAutoRedirect = true,
            UseDefaultCredentials = true
        });

Cookies

Le regroupement des instances HttpMessageHandler engendre le partage d’objets CookieContainer. Le partage d’objets CookieContainer imprévus entraîne souvent un code incorrect. Pour les applications qui nécessitent des cookie, tenez compte de ce qui suit :

  • Désactivation de la gestion de cookie automatique
  • Éviter IHttpClientFactory

Appelez ConfigurePrimaryHttpMessageHandler pour désactiver la gestion de cookie automatique :

builder.Services.AddHttpClient("NoAutomaticCookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            UseCookies = false
        });

Utiliser IHttpClientFactory dans une application console

Dans une application console, ajoutez les références de package suivantes au projet :

Dans l’exemple suivant :

  • IHttpClientFactory et GitHubService sont inscrits dans le conteneur de service dans de l’hôte générique.
  • GitHubService est demandé à la DI, qui demande à son tour une instance de IHttpClientFactory.
  • GitHubService utilise IHttpClientFactory pour créer une instance de HttpClient, qu’elle utilise pour récupérer des branches GitHub de documentation.
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var host = new HostBuilder()
    .ConfigureServices(services =>
    {
        services.AddHttpClient();
        services.AddTransient<GitHubService>();
    })
    .Build();

try
{
    var gitHubService = host.Services.GetRequiredService<GitHubService>();
    var gitHubBranches = await gitHubService.GetAspNetCoreDocsBranchesAsync();

    Console.WriteLine($"{gitHubBranches?.Count() ?? 0} GitHub Branches");

    if (gitHubBranches is not null)
    {
        foreach (var gitHubBranch in gitHubBranches)
        {
            Console.WriteLine($"- {gitHubBranch.Name}");
        }
    }
}
catch (Exception ex)
{
    host.Services.GetRequiredService<ILogger<Program>>()
        .LogError(ex, "Unable to load branches from GitHub.");
}

public class GitHubService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public GitHubService(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { "Accept", "application/vnd.github.v3+json" },
                { "User-Agent", "HttpRequestsConsoleSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        httpResponseMessage.EnsureSuccessStatusCode();

        using var contentStream =
            await httpResponseMessage.Content.ReadAsStreamAsync();
        
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubBranch>>(contentStream);
    }
}

public record GitHubBranch(
    [property: JsonPropertyName("name")] string Name);

Intergiciel de propagation d’en-tête

La propagation d’en-tête est un intergiciel ASP.NET Core pour propager les en-têtes HTTP de la requête entrante vers les requêtes HttpClient sortantes. Pour utiliser la propagation d’en-tête :

  • Installez le package Microsoft.AspNetCore.HeaderPropagation.

  • Configurez le pipeline de HttpClient et d’intergiciel dans Program.cs :

    // Add services to the container.
    builder.Services.AddControllers();
    
    builder.Services.AddHttpClient("PropagateHeaders")
        .AddHeaderPropagation();
    
    builder.Services.AddHeaderPropagation(options =>
    {
        options.Headers.Add("X-TraceId");
    });
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    app.UseHttpsRedirection();
    
    app.UseHeaderPropagation();
    
    app.MapControllers();
    
  • Effectuez des requêtes sortantes à l’aide de l’instance HttpClient configurée, qui inclut les en-têtes ajoutés.

Ressources supplémentaires

Par Kirk Larkin, Steve Gordon, Glenn Condron et Ryan Nowak.

Une IHttpClientFactory peut être inscrite et utilisée pour configurer et créer des instances de HttpClient dans une application. IHttpClientFactory offre les avantages suivants :

  • Fournit un emplacement central pour le nommage et la configuration d’instance de HttpClient logiques. Par exemple, un client nommé github peut être inscrit et configuré pour accéder à GitHub. Un client par défaut peut être inscrit pour l’accès général.
  • Codifie le concept d’intergiciel (middleware) sortant via la délégation de gestionnaires dans HttpClient. Fournit des extensions pour les intergiciels basés sur Polly afin de tirer parti de la délégation de gestionnaires dans HttpClient.
  • Gère les pools et la durée de vie des instances HttpClientMessageHandler sous-jacentes. La gestion automatique évite les problèmes courants liés au système DNS (Domain Name System) qui se produisent lors de la gestion manuelle des durées de vie HttpClient.
  • Ajoute une expérience de journalisation configurable (via ILogger) pour toutes les requêtes envoyées via des clients créés par la fabrique.

Affichez ou téléchargez un exemple de code (procédure de téléchargement).

L’exemple de code de cette version de rubrique utilise System.Text.Json pour désérialiser le contenu JSON retourné dans les réponses HTTP. Pour les exemples qui utilisent Json.NET et ReadAsAsync<T>, utilisez le sélecteur de version pour sélectionner une version 2.x de cette rubrique.

Modèles de consommation

Vous pouvez utiliser IHttpClientFactory dans une application de plusieurs façons :

La meilleure approche dépend des exigences de l’application.

Utilisation de base

IHttpClientFactory peut être inscrit en appelant AddHttpClient :

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

Une IHttpClientFactory peut être demandée à l’aide de l’injection de dépendances (DI). Le code suivant utilise IHttpClientFactory pour créer une instance HttpClient :

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

L’utilisation de IHttpClientFactory comme dans l’exemple précédent est un bon moyen de refactoriser une application existante. Cela n’a aucun impact sur la façon dont HttpClient est utilisé. Aux endroits où des instances HttpClient sont créées dans une application existante, remplacez ces occurrences par des appels à CreateClient.

Clients nommés

Les clients nommés sont un bon choix dans les cas suivants :

  • L’application nécessite de nombreuses utilisations distinctes de HttpClient.
  • De nombreuses instances HttpClient ont une configuration différente.

La configuration d’un HttpClient nommé peut être spécifiée lors de l’inscription dans Startup.ConfigureServices :

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

Dans le code précédent, le client est configuré avec :

  • L’adresse de base https://api.github.com/.
  • Deux en-têtes requis pour fonctionner avec l’API GitHub.

CreateClient

Chaque fois que CreateClient est appelé :

  • Une nouvelle instance de HttpClient est créée.
  • L’action de configuration est appelée.

Pour créer un client nommé, passez son nom dans CreateClient :

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

Dans le code précédent, la requête n’a pas besoin de spécifier un nom d’hôte. Le code peut simplement passer le chemin, car l’adresse de base configurée pour le client est utilisée.

Clients typés

Clients typés :

  • Fournissent les mêmes fonctionnalités que les clients nommés, sans qu’il soit nécessaire d’utiliser des chaînes comme clés.
  • Bénéficie de l’aide d’IntelliSense et du compilateur lors de l’utilisation des clients.
  • Fournissent un emplacement unique pour configurer et interagir avec un HttpClient particulier. Par exemple, un client typé unique peut être utilisé :
    • Pour un point de terminaison principal unique.
    • Pour encapsuler toute la logique traitant du point de terminaison.
  • Pour travailler avec l’injection de dépendances et injecter là où c’est nécessaire dans l’application.

Un client typé accepte un paramètre HttpClient dans son constructeur :

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        return await Client.GetFromJsonAsync<IEnumerable<GitHubIssue>>(
          "/repos/aspnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");
    }
}

Dans le code précédent :

  • La configuration est déplacée dans le client typé.
  • L’objet HttpClient est exposé en tant que propriété publique.

Vous pouvez créer des méthodes spécifiques à l’API qui exposent des fonctionnalités de HttpClient. Par exemple, la méthode encapsule le code GetAspNetDocsIssues pour récupérer des problèmes ouverts.

Le code suivant appelle AddHttpClient dans Startup.ConfigureServices pour inscrire une classe cliente typée :

services.AddHttpClient<GitHubService>();

Le client typé est inscrit comme étant transitoire avec injection de dépendances. Dans le code précédent, AddHttpClient inscrit GitHubService en tant que service temporaire. Cette inscription utilise une méthode de fabrique pour :

  1. Créez une instance de HttpClient.
  2. Créer une instance de GitHubService en passant l’instance de HttpClient à son constructeur.

Le client typé peut être injecté et utilisé directement :

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

Vous pouvez spécifier la configuration d’un client typé lors de l’inscription dans Startup.ConfigureServices au lieu de le faire dans le constructeur du client typé :

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

HttpClient peut être encapsulé dans un client typé. Au lieu de l’exposer en tant que propriété, définissez une méthode qui appelle l’instance HttpClient en interne :

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

Dans le code précédent, le HttpClient est stocké dans un champ privé. L’accès à HttpClient se fait par la méthode publique GetRepos.

Clients générés

IHttpClientFactory peut être utilisé en combinaison avec des bibliothèques tierces, comme Refit. Refit est une bibliothèque REST pour .NET. Il convertit les API REST en interfaces dynamiques. Une implémentation de l’interface est générée dynamiquement par le RestService, avec HttpClient pour faire les appels HTTP externes.

Une interface et une réponse sont définies pour représenter l’API externe et sa réponse :

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

Vous pouvez ajouter un client typé en utilisant Refit pour générer l’implémentation :

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

L’interface définie peut être utilisée quand c’est nécessaire, avec l’implémentation fournie par l’injection de dépendances et Refit :

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

Effectuer des requêtes POST, PUT et DELETE

Dans les exemples précédents, toutes les requêtes HTTP utilisent le verbe HTTP GET. HttpClient prend également en charge d’autres verbes HTTP, notamment :

  • POST
  • PUT
  • DELETE
  • PATCH

Pour obtenir la liste complète des verbes HTTP pris en charge, consultez HttpMethod.

L’exemple suivant montre comment effectuer une requête HTTP POST :

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

Dans le code précédent, la méthode CreateItemAsync :

  • Sérialise le paramètre TodoItem au format JSON à l’aide de System.Text.Json. Cela utilise une instance de JsonSerializerOptions pour configurer le processus de sérialisation.
  • Crée une instance de StringContent pour empaqueter le JSON sérialisé pour l’envoi dans le corps de la requête HTTP.
  • Appelle PostAsync pour envoyer le contenu JSON à l’URL spécifiée. Il s’agit d’une URL relative qui est ajoutée à HttpClient.BaseAddress.
  • Appelle EnsureSuccessStatusCode pour lever une exception si le code d’état de la réponse n’indique pas la réussite.

HttpClient prend également en charge d’autres types de contenu. Par exemple : MultipartContent et StreamContent. Pour obtenir la liste complète du contenu pris en charge, consultez HttpContent.

L’exemple suivant montre une requête HTTP PUT :

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

Le code précédent est très similaire à l’exemple POST. La méthode SaveItemAsync appelle PutAsync au lieu de PostAsync.

L’exemple suivant montre une requête HTTP DELETE :

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponse.EnsureSuccessStatusCode();
}

Dans le code précédent, la méthode DeleteItemAsync appelle DeleteAsync. Étant donné que les requêtes HTTP DELETE ne contiennent généralement aucun corps, la méthode DeleteAsync ne fournit pas de surcharge qui accepte une instance de HttpContent.

Pour en savoir plus sur l’utilisation de différents verbes HTTP avec HttpClient, consultez HttpClient.

Middleware pour les requêtes sortantes

HttpClient intègre le concept de délégation des gestionnaires qui peuvent être liés ensemble pour les requêtes HTTP sortantes. IHttpClientFactory:

  • Simplifie la définition des gestionnaires à appliquer pour chaque client nommé.
  • Il prend en charge l’inscription et le chaînage de plusieurs gestionnaires pour créer un pipeline d’intergiciels pour les requêtes sortantes. Chacun de ces gestionnaires peut effectuer un travail avant et après la requête sortante. Ce modèle :
    • Est similaire au pipeline d’intergiciels entrants dans ASP.NET Core.
    • Fournit un mécanisme pour gérer les problèmes transversaux liés aux requêtes HTTP, par exemple :
      • mise en cache
      • gestion des erreurs
      • sérialisation
      • journalisation

Pour créer un gestionnaire de délégation :

  • Dérivez de DelegatingHandler.
  • Remplacez SendAsync. Exécutez du code avant de passer la requête au gestionnaire suivant dans le pipeline :
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Le code précédent vérifie si l’en-tête X-API-KEY se trouve dans la requête. Si X-API-KEY est manquant, BadRequest est retourné.

Plusieurs gestionnaires peuvent être ajoutés à la configuration d’un HttpClient avec Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler :

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

Dans le code précédent, le ValidateHeaderHandler est inscrit avec une injection de dépendances. Une fois inscrit, AddHttpMessageHandler peut être appelé en passant en entrée le type pour le gestionnaire.

Vous pouvez inscrire plusieurs gestionnaires dans l’ordre où ils doivent être exécutés. Chaque gestionnaire wrappe le gestionnaire suivant jusqu’à ce que le dernier HttpClientHandler exécute la requête :

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Utiliser l’intergiciel de requêtes sortantes

Lorsque IHttpClientFactory crée un gestionnaire de délégation, il utilise l’injection de dépendances pour remplir les paramètres du constructeur du gestionnaire. IHttpClientFactory crée une étendue de DI distincte pour chaque gestionnaire, ce qui peut entraîner un comportement surprenant lorsqu’un gestionnaire consomme un service délimité.

Par exemple, considérez l’interface suivante et son implémentation, qui représente une tâche en tant qu’opération avec un identificateur, OperationId :

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Comme son nom l’indique, IOperationScoped est inscrit avec la DI à l’aide d’une durée de vie étendue :

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

Le gestionnaire de délégation suivant consomme et utilise IOperationScoped pour définir l’en-tête X-OPERATION-ID pour la requête sortante :

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

Dans le Téléchargement de HttpRequestsSample], accédez à /Operation et actualisez la page. La valeur de l’étendue de la requête change pour chaque requête, mais la valeur d’étendue du gestionnaire change uniquement toutes les 5 secondes.

Les gestionnaires peuvent dépendre des services de n’importe quelle étendue. Les services dont dépendent les gestionnaires sont supprimés lorsque le gestionnaire est supprimé.

Utilisez l’une des approches suivantes pour partager l’état de chaque requête avec les gestionnaires de messages :

Utiliser les gestionnaires Polly

IHttpClientFactory s’intègre à la bibliothèque tierce Polly. Polly est une bibliothèque complète de gestion des erreurs transitoires et de résilience pour .NET. Elle permet aux développeurs de formuler facilement et de façon thread-safe des stratégies, comme Retry (Nouvelle tentative), Circuit Breaker (Disjoncteur), Timeout (Délai d’attente), Bulkhead Isolation (Isolation par cloisonnement) et Fallback (Alternative de repli).

Des méthodes d’extension sont fournies pour permettre l’utilisation de stratégies Polly avec les instances configurées de HttpClient. Les extensions Polly prennent en charge l’ajout de gestionnaires basés sur Polly aux clients. Polly nécessite le package NuGet Microsoft.Extensions.Http.Polly.

Gérer les erreurs temporaires

Les erreurs se produisent généralement lorsque des appels HTTP externes sont temporaires. AddTransientHttpErrorPolicy permet à une stratégie d’être définie pour gérer les erreurs temporaires. Les stratégies configurées avec AddTransientHttpErrorPolicy gèrent les réponses suivantes :

AddTransientHttpErrorPolicy fournit l’accès à un objet PolicyBuilder configuré pour gérer les erreurs représentant une erreur temporaire possible :

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

Dans le code précédent, une stratégie WaitAndRetryAsync est définie. Les requêtes qui ont échoué sont retentées jusqu’à trois fois avec un délai de 600 ms entre les tentatives.

Sélectionner dynamiquement des stratégies

Des méthodes d’extension sont fournies pour ajouter des gestionnaires basés sur Polly, par exemple AddPolicyHandler. La surcharge de AddPolicyHandler suivante inspecte la requête pour déterminer la stratégie à appliquer :

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

Dans le code précédent, si la requête sortante est un HTTP GET, un délai d’attente de 10 secondes est appliqué. Pour toutes les autres méthodes HTTP, un délai d’attente de 30 secondes est utilisé.

Ajouter plusieurs gestionnaires Polly

Il est courant d’imbriquer les stratégies Polly :

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

Dans l’exemple précédent :

  • Deux gestionnaires sont ajoutés.
  • Le premier gestionnaire utilise AddTransientHttpErrorPolicy pour ajouter une stratégie de nouvelle tentative. Les requêtes qui ont échoué sont retentées jusqu’à trois fois.
  • Le deuxième appel à AddTransientHttpErrorPolicy ajoute une stratégie de disjoncteur. Les requêtes externes supplémentaires sont bloquées pendant 30 secondes si 5 tentatives successives échouent. Les stratégies de disjoncteur sont avec état. Tous les appels effectués via ce client partagent le même état du circuit.

Ajouter des stratégies à partir du Registre Polly

Une approche de la gestion des stratégies régulièrement utilisées consiste à les définir une seule fois et à les inscrire avec un PolicyRegistry.

Dans le code suivant :

  • Les stratégies « regular » et « long » sont ajoutées.
  • AddPolicyHandlerFromRegistry ajoute les stratégies « regular » et « long » à partir du Registre.
public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

Pour plus d’informations sur les intégrations IHttpClientFactory et Polly, consultez le Wiki Polly.

HttpClient et gestion de la durée de vie

Une nouvelle instance HttpClient est retournée à chaque fois que CreateClient est appelé sur IHttpClientFactory. Un HttpMessageHandler est créé par client nommé. La fabrique gère les durées de vie des instances HttpMessageHandler.

IHttpClientFactory regroupe dans un pool les instances de HttpMessageHandler créées par la fabrique pour réduire la consommation des ressources. Une instance de HttpMessageHandler peut être réutilisée à partir du pool quand vous créez une instance de HttpClient si sa durée de vie n’a pas expiré.

Le regroupement de gestionnaires en pools est souhaitable, car chaque gestionnaire gère habituellement ses propres connexions HTTP sous-jacentes. La création de plus de gestionnaires que nécessaire peut entraîner des délais de connexion. Certains gestionnaires conservent aussi les connexions indéfiniment ouvertes, ce qui peut empêcher le gestionnaire de réagir aux changements du DNS (Domain Name System).

La durée de vie par défaut d’un gestionnaire est de deux minutes. La valeur par défaut peut être remplacée pour chaque client nommé :

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

Les instances HttpClient peuvent généralement être traitées en tant qu’objets .NET ne nécessitant pas une suppression. La suppression annule les requêtes sortantes et garantit que l’instance HttpClient donnée ne peut pas être utilisée après avoir appelé Dispose. IHttpClientFactory effectue le suivi et libère les ressources utilisées par les instances HttpClient.

Le fait de conserver une seule instance HttpClient active pendant une longue durée est un modèle commun utilisé avant le lancement de IHttpClientFactory. Ce modèle devient inutile après la migration vers IHttpClientFactory.

Alternatives à IHttpClientFactory

L’utilisation de IHttpClientFactory dans une application avec injection de dépendances évite :

  • Les problèmes d’épuisement des ressources en regroupant les instances HttpMessageHandler.
  • Les problèmes de DNS obsolète en permutant les instances HttpMessageHandler à intervalles réguliers.

Il existe d’autres façons de résoudre les problèmes précédents à l’aide d’une instance SocketsHttpHandler de longue durée.

  • Créez une instance de SocketsHttpHandler lorsque l’application démarre et utilisez-la pour la durée de vie de l’application.
  • Configurez PooledConnectionLifetime sur une valeur appropriée en fonction des temps d’actualisation DNS.
  • Créez des instances HttpClient à l’aide de new HttpClient(handler, disposeHandler: false) si nécessaire.

Les approches précédentes résolvent les problèmes de gestion des ressources que IHttpClientFactory résout de la même manière.

  • Le SocketsHttpHandler partage les connexions entre les instances HttpClient. Ce partage empêche l’épuisement des sockets.
  • Le SocketsHttpHandler permute les connexions en fonction de PooledConnectionLifetime pour éviter les problèmes de DNS obsolète.

Cookies

Le regroupement des instances HttpMessageHandler engendre le partage d’objets CookieContainer. Le partage d’objets CookieContainer imprévus entraîne souvent un code incorrect. Pour les applications qui nécessitent des cookie, tenez compte de ce qui suit :

  • Désactivation de la gestion de cookie automatique
  • Éviter IHttpClientFactory

Appelez ConfigurePrimaryHttpMessageHandler pour désactiver la gestion de cookie automatique :

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Journalisation

Les clients créés via IHttpClientFactory enregistrent les messages de journalisation pour toutes les requêtes. Activez le niveau d’informations approprié dans la configuration de journalisation pour voir les messages de journalisation par défaut. Une journalisation supplémentaire, comme celle des en-têtes des requêtes, est incluse seulement au niveau de trace.

La catégorie de journal utilisée pour chaque client comprend le nom du client. Par exemple, un client nommé MyNamedClient journalise les messages avec la catégorie « System.Net.Http.HttpClient.MyNamedClient.LogicalHandler ». Les messages avec le suffixe LogicalHandler se produisent en dehors du pipeline du gestionnaire de requêtes. Lors d’une requête, les messages sont journalisés avant que d’autres gestionnaires du pipeline l’aient traitée. Lors d’une réponse, les messages sont journalisés une fois que tous les autres gestionnaires du pipeline ont reçu la réponse.

La journalisation se produit également à l’intérieur du pipeline du gestionnaire de requêtes. Dans l’exemple MyNamedClient, ces messages sont enregistrés avec la catégorie de journal « System.Net.Http.HttpClient.MyNamedClient.ClientHandler ». Pour la requête, cela se produit après que tous les autres gestionnaires ont été exécutés et immédiatement avant l’envoi de la requête. Lors de la réponse, cette journalisation inclut l’état de la réponse avant qu’elle repasse à travers le pipeline de gestionnaires.

L’activation de la journalisation à l’extérieur et à l’intérieur du pipeline permet l’inspection des changements apportés par les autres gestionnaires du pipeline. Cela peut comprendre des changements apportés aux en-têtes des requêtes ou au code d’état de la réponse.

L’ajout du nom du client dans la catégorie de journalisation permet de filtrer le journal pour des clients nommés spécifiques.

Configurer le HttpMessageHandler

Il peut être nécessaire de contrôler la configuration du HttpMessageHandler interne utilisé par un client.

Un IHttpClientBuilder est retourné quand vous ajoutez des clients nommés ou typés. La méthode d’extension ConfigurePrimaryHttpMessageHandler peut être utilisée pour définir un délégué. Le délégué est utilisé pour créer et configurer le HttpMessageHandler principal utilisé par ce client :

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

Utiliser IHttpClientFactory dans une application console

Dans une application console, ajoutez les références de package suivantes au projet :

Dans l’exemple suivant :

  • IHttpClientFactory est inscrit dans le conteneur de service dans de l’hôte générique.
  • MyService crée une instance de fabrique cliente à partir du service, qui est utilisée pour créer un HttpClient. HttpClient est utilisé pour récupérer une page web.
  • Main crée une étendue pour exécuter la méthode GetPage du service et écrire les 500 premiers caractères du contenu de la page web dans la console.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

Intergiciel de propagation d’en-tête

La propagation d’en-tête est un intergiciel ASP.NET Core pour propager les en-têtes HTTP de la requête entrante vers les requêtes client HTTP sortantes. Pour utiliser la propagation d’en-tête :

  • Référencez le package Microsoft.AspNetCore.HeaderPropagation.

  • Configurez l’intergiciel et HttpClient dans Startup :

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseRouting();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  • Le client inclut les en-têtes configurés sur les requêtes sortantes :

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Ressources supplémentaires

Par Kirk Larkin, Steve Gordon, Glenn Condron et Ryan Nowak.

Une IHttpClientFactory peut être inscrite et utilisée pour configurer et créer des instances de HttpClient dans une application. IHttpClientFactory offre les avantages suivants :

  • Fournit un emplacement central pour le nommage et la configuration d’instance de HttpClient logiques. Par exemple, un client nommé github peut être inscrit et configuré pour accéder à GitHub. Un client par défaut peut être inscrit pour l’accès général.
  • Codifie le concept d’intergiciel (middleware) sortant via la délégation de gestionnaires dans HttpClient. Fournit des extensions pour les intergiciels basés sur Polly afin de tirer parti de la délégation de gestionnaires dans HttpClient.
  • Gère les pools et la durée de vie des instances HttpClientMessageHandler sous-jacentes. La gestion automatique évite les problèmes courants liés au système DNS (Domain Name System) qui se produisent lors de la gestion manuelle des durées de vie HttpClient.
  • Ajoute une expérience de journalisation configurable (via ILogger) pour toutes les requêtes envoyées via des clients créés par la fabrique.

Affichez ou téléchargez un exemple de code (procédure de téléchargement).

L’exemple de code de cette version de rubrique utilise System.Text.Json pour désérialiser le contenu JSON retourné dans les réponses HTTP. Pour les exemples qui utilisent Json.NET et ReadAsAsync<T>, utilisez le sélecteur de version pour sélectionner une version 2.x de cette rubrique.

Modèles de consommation

Vous pouvez utiliser IHttpClientFactory dans une application de plusieurs façons :

La meilleure approche dépend des exigences de l’application.

Utilisation de base

IHttpClientFactory peut être inscrit en appelant AddHttpClient :

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

Une IHttpClientFactory peut être demandée à l’aide de l’injection de dépendances (DI). Le code suivant utilise IHttpClientFactory pour créer une instance HttpClient :

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

L’utilisation de IHttpClientFactory comme dans l’exemple précédent est un bon moyen de refactoriser une application existante. Cela n’a aucun impact sur la façon dont HttpClient est utilisé. Aux endroits où des instances HttpClient sont créées dans une application existante, remplacez ces occurrences par des appels à CreateClient.

Clients nommés

Les clients nommés sont un bon choix dans les cas suivants :

  • L’application nécessite de nombreuses utilisations distinctes de HttpClient.
  • De nombreuses instances HttpClient ont une configuration différente.

La configuration d’un HttpClient nommé peut être spécifiée lors de l’inscription dans Startup.ConfigureServices :

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

Dans le code précédent, le client est configuré avec :

  • L’adresse de base https://api.github.com/.
  • Deux en-têtes requis pour fonctionner avec l’API GitHub.

CreateClient

Chaque fois que CreateClient est appelé :

  • Une nouvelle instance de HttpClient est créée.
  • L’action de configuration est appelée.

Pour créer un client nommé, passez son nom dans CreateClient :

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

Dans le code précédent, la requête n’a pas besoin de spécifier un nom d’hôte. Le code peut simplement passer le chemin, car l’adresse de base configurée pour le client est utilisée.

Clients typés

Clients typés :

  • Fournissent les mêmes fonctionnalités que les clients nommés, sans qu’il soit nécessaire d’utiliser des chaînes comme clés.
  • Bénéficie de l’aide d’IntelliSense et du compilateur lors de l’utilisation des clients.
  • Fournissent un emplacement unique pour configurer et interagir avec un HttpClient particulier. Par exemple, un client typé unique peut être utilisé :
    • Pour un point de terminaison principal unique.
    • Pour encapsuler toute la logique traitant du point de terminaison.
  • Pour travailler avec l’injection de dépendances et injecter là où c’est nécessaire dans l’application.

Un client typé accepte un paramètre HttpClient dans son constructeur :

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubIssue>>(responseStream);
    }
}

Si vous souhaitez voir les commentaires de code traduits dans une langue autre que l’anglais, dites-le nous dans cette discussion GitHub.

Dans le code précédent :

  • La configuration est déplacée dans le client typé.
  • L’objet HttpClient est exposé en tant que propriété publique.

Vous pouvez créer des méthodes spécifiques à l’API qui exposent des fonctionnalités de HttpClient. Par exemple, la méthode encapsule le code GetAspNetDocsIssues pour récupérer des problèmes ouverts.

Le code suivant appelle AddHttpClient dans Startup.ConfigureServices pour inscrire une classe cliente typée :

services.AddHttpClient<GitHubService>();

Le client typé est inscrit comme étant transitoire avec injection de dépendances. Dans le code précédent, AddHttpClient inscrit GitHubService en tant que service temporaire. Cette inscription utilise une méthode de fabrique pour :

  1. Créez une instance de HttpClient.
  2. Créer une instance de GitHubService en passant l’instance de HttpClient à son constructeur.

Le client typé peut être injecté et utilisé directement :

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

Vous pouvez spécifier la configuration d’un client typé lors de l’inscription dans Startup.ConfigureServices au lieu de le faire dans le constructeur du client typé :

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

HttpClient peut être encapsulé dans un client typé. Au lieu de l’exposer en tant que propriété, définissez une méthode qui appelle l’instance HttpClient en interne :

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

Dans le code précédent, le HttpClient est stocké dans un champ privé. L’accès à HttpClient se fait par la méthode publique GetRepos.

Clients générés

IHttpClientFactory peut être utilisé en combinaison avec des bibliothèques tierces, comme Refit. Refit est une bibliothèque REST pour .NET. Il convertit les API REST en interfaces dynamiques. Une implémentation de l’interface est générée dynamiquement par le RestService, avec HttpClient pour faire les appels HTTP externes.

Une interface et une réponse sont définies pour représenter l’API externe et sa réponse :

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

Vous pouvez ajouter un client typé en utilisant Refit pour générer l’implémentation :

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

L’interface définie peut être utilisée quand c’est nécessaire, avec l’implémentation fournie par l’injection de dépendances et Refit :

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

Effectuer des requêtes POST, PUT et DELETE

Dans les exemples précédents, toutes les requêtes HTTP utilisent le verbe HTTP GET. HttpClient prend également en charge d’autres verbes HTTP, notamment :

  • POST
  • PUT
  • DELETE
  • PATCH

Pour obtenir la liste complète des verbes HTTP pris en charge, consultez HttpMethod.

L’exemple suivant montre comment effectuer une requête HTTP POST :

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

Dans le code précédent, la méthode CreateItemAsync :

  • Sérialise le paramètre TodoItem au format JSON à l’aide de System.Text.Json. Cela utilise une instance de JsonSerializerOptions pour configurer le processus de sérialisation.
  • Crée une instance de StringContent pour empaqueter le JSON sérialisé pour l’envoi dans le corps de la requête HTTP.
  • Appelle PostAsync pour envoyer le contenu JSON à l’URL spécifiée. Il s’agit d’une URL relative qui est ajoutée à HttpClient.BaseAddress.
  • Appelle EnsureSuccessStatusCode pour lever une exception si le code d’état de la réponse n’indique pas la réussite.

HttpClient prend également en charge d’autres types de contenu. Par exemple : MultipartContent et StreamContent. Pour obtenir la liste complète du contenu pris en charge, consultez HttpContent.

L’exemple suivant montre une requête HTTP PUT :

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

Le code précédent est très similaire à l’exemple POST. La méthode SaveItemAsync appelle PutAsync au lieu de PostAsync.

L’exemple suivant montre une requête HTTP DELETE :

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponse.EnsureSuccessStatusCode();
}

Dans le code précédent, la méthode DeleteItemAsync appelle DeleteAsync. Étant donné que les requêtes HTTP DELETE ne contiennent généralement aucun corps, la méthode DeleteAsync ne fournit pas de surcharge qui accepte une instance de HttpContent.

Pour en savoir plus sur l’utilisation de différents verbes HTTP avec HttpClient, consultez HttpClient.

Middleware pour les requêtes sortantes

HttpClient intègre le concept de délégation des gestionnaires qui peuvent être liés ensemble pour les requêtes HTTP sortantes. IHttpClientFactory:

  • Simplifie la définition des gestionnaires à appliquer pour chaque client nommé.
  • Il prend en charge l’inscription et le chaînage de plusieurs gestionnaires pour créer un pipeline d’intergiciels pour les requêtes sortantes. Chacun de ces gestionnaires peut effectuer un travail avant et après la requête sortante. Ce modèle :
    • Est similaire au pipeline d’intergiciels entrants dans ASP.NET Core.
    • Fournit un mécanisme pour gérer les problèmes transversaux liés aux requêtes HTTP, par exemple :
      • mise en cache
      • gestion des erreurs
      • sérialisation
      • journalisation

Pour créer un gestionnaire de délégation :

  • Dérivez de DelegatingHandler.
  • Remplacez SendAsync. Exécutez du code avant de passer la requête au gestionnaire suivant dans le pipeline :
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Le code précédent vérifie si l’en-tête X-API-KEY se trouve dans la requête. Si X-API-KEY est manquant, BadRequest est retourné.

Plusieurs gestionnaires peuvent être ajoutés à la configuration d’un HttpClient avec Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler :

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

Dans le code précédent, le ValidateHeaderHandler est inscrit avec une injection de dépendances. Une fois inscrit, AddHttpMessageHandler peut être appelé en passant en entrée le type pour le gestionnaire.

Vous pouvez inscrire plusieurs gestionnaires dans l’ordre où ils doivent être exécutés. Chaque gestionnaire wrappe le gestionnaire suivant jusqu’à ce que le dernier HttpClientHandler exécute la requête :

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Utiliser l’intergiciel de requêtes sortantes

Lorsque IHttpClientFactory crée un gestionnaire de délégation, il utilise l’injection de dépendances pour remplir les paramètres du constructeur du gestionnaire. IHttpClientFactory crée une étendue de DI distincte pour chaque gestionnaire, ce qui peut entraîner un comportement surprenant lorsqu’un gestionnaire consomme un service délimité.

Par exemple, considérez l’interface suivante et son implémentation, qui représente une tâche en tant qu’opération avec un identificateur, OperationId :

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Comme son nom l’indique, IOperationScoped est inscrit avec la DI à l’aide d’une durée de vie étendue :

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

Le gestionnaire de délégation suivant consomme et utilise IOperationScoped pour définir l’en-tête X-OPERATION-ID pour la requête sortante :

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

Dans le Téléchargement de HttpRequestsSample], accédez à /Operation et actualisez la page. La valeur de l’étendue de la requête change pour chaque requête, mais la valeur d’étendue du gestionnaire change uniquement toutes les 5 secondes.

Les gestionnaires peuvent dépendre des services de n’importe quelle étendue. Les services dont dépendent les gestionnaires sont supprimés lorsque le gestionnaire est supprimé.

Utilisez l’une des approches suivantes pour partager l’état de chaque requête avec les gestionnaires de messages :

Utiliser les gestionnaires Polly

IHttpClientFactory s’intègre à la bibliothèque tierce Polly. Polly est une bibliothèque complète de gestion des erreurs transitoires et de résilience pour .NET. Elle permet aux développeurs de formuler facilement et de façon thread-safe des stratégies, comme Retry (Nouvelle tentative), Circuit Breaker (Disjoncteur), Timeout (Délai d’attente), Bulkhead Isolation (Isolation par cloisonnement) et Fallback (Alternative de repli).

Des méthodes d’extension sont fournies pour permettre l’utilisation de stratégies Polly avec les instances configurées de HttpClient. Les extensions Polly prennent en charge l’ajout de gestionnaires basés sur Polly aux clients. Polly nécessite le package NuGet Microsoft.Extensions.Http.Polly.

Gérer les erreurs temporaires

Les erreurs se produisent généralement lorsque des appels HTTP externes sont temporaires. AddTransientHttpErrorPolicy permet à une stratégie d’être définie pour gérer les erreurs temporaires. Les stratégies configurées avec AddTransientHttpErrorPolicy gèrent les réponses suivantes :

AddTransientHttpErrorPolicy fournit l’accès à un objet PolicyBuilder configuré pour gérer les erreurs représentant une erreur temporaire possible :

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

Dans le code précédent, une stratégie WaitAndRetryAsync est définie. Les requêtes qui ont échoué sont retentées jusqu’à trois fois avec un délai de 600 ms entre les tentatives.

Sélectionner dynamiquement des stratégies

Des méthodes d’extension sont fournies pour ajouter des gestionnaires basés sur Polly, par exemple AddPolicyHandler. La surcharge de AddPolicyHandler suivante inspecte la requête pour déterminer la stratégie à appliquer :

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

Dans le code précédent, si la requête sortante est un HTTP GET, un délai d’attente de 10 secondes est appliqué. Pour toutes les autres méthodes HTTP, un délai d’attente de 30 secondes est utilisé.

Ajouter plusieurs gestionnaires Polly

Il est courant d’imbriquer les stratégies Polly :

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

Dans l’exemple précédent :

  • Deux gestionnaires sont ajoutés.
  • Le premier gestionnaire utilise AddTransientHttpErrorPolicy pour ajouter une stratégie de nouvelle tentative. Les requêtes qui ont échoué sont retentées jusqu’à trois fois.
  • Le deuxième appel à AddTransientHttpErrorPolicy ajoute une stratégie de disjoncteur. Les requêtes externes supplémentaires sont bloquées pendant 30 secondes si 5 tentatives successives échouent. Les stratégies de disjoncteur sont avec état. Tous les appels effectués via ce client partagent le même état du circuit.

Ajouter des stratégies à partir du Registre Polly

Une approche de la gestion des stratégies régulièrement utilisées consiste à les définir une seule fois et à les inscrire avec un PolicyRegistry.

Dans le code suivant :

  • Les stratégies « regular » et « long » sont ajoutées.
  • AddPolicyHandlerFromRegistry ajoute les stratégies « regular » et « long » à partir du Registre.
public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

Pour plus d’informations sur les intégrations IHttpClientFactory et Polly, consultez le Wiki Polly.

HttpClient et gestion de la durée de vie

Une nouvelle instance HttpClient est retournée à chaque fois que CreateClient est appelé sur IHttpClientFactory. Un HttpMessageHandler est créé par client nommé. La fabrique gère les durées de vie des instances HttpMessageHandler.

IHttpClientFactory regroupe dans un pool les instances de HttpMessageHandler créées par la fabrique pour réduire la consommation des ressources. Une instance de HttpMessageHandler peut être réutilisée à partir du pool quand vous créez une instance de HttpClient si sa durée de vie n’a pas expiré.

Le regroupement de gestionnaires en pools est souhaitable, car chaque gestionnaire gère habituellement ses propres connexions HTTP sous-jacentes. La création de plus de gestionnaires que nécessaire peut entraîner des délais de connexion. Certains gestionnaires conservent aussi les connexions indéfiniment ouvertes, ce qui peut empêcher le gestionnaire de réagir aux changements du DNS (Domain Name System).

La durée de vie par défaut d’un gestionnaire est de deux minutes. La valeur par défaut peut être remplacée pour chaque client nommé :

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

Les instances HttpClient peuvent généralement être traitées en tant qu’objets .NET ne nécessitant pas une suppression. La suppression annule les requêtes sortantes et garantit que l’instance HttpClient donnée ne peut pas être utilisée après avoir appelé Dispose. IHttpClientFactory effectue le suivi et libère les ressources utilisées par les instances HttpClient.

Le fait de conserver une seule instance HttpClient active pendant une longue durée est un modèle commun utilisé avant le lancement de IHttpClientFactory. Ce modèle devient inutile après la migration vers IHttpClientFactory.

Alternatives à IHttpClientFactory

L’utilisation de IHttpClientFactory dans une application avec injection de dépendances évite :

  • Les problèmes d’épuisement des ressources en regroupant les instances HttpMessageHandler.
  • Les problèmes de DNS obsolète en permutant les instances HttpMessageHandler à intervalles réguliers.

Il existe d’autres façons de résoudre les problèmes précédents à l’aide d’une instance SocketsHttpHandler de longue durée.

  • Créez une instance de SocketsHttpHandler lorsque l’application démarre et utilisez-la pour la durée de vie de l’application.
  • Configurez PooledConnectionLifetime sur une valeur appropriée en fonction des temps d’actualisation DNS.
  • Créez des instances HttpClient à l’aide de new HttpClient(handler, disposeHandler: false) si nécessaire.

Les approches précédentes résolvent les problèmes de gestion des ressources que IHttpClientFactory résout de la même manière.

  • Le SocketsHttpHandler partage les connexions entre les instances HttpClient. Ce partage empêche l’épuisement des sockets.
  • Le SocketsHttpHandler permute les connexions en fonction de PooledConnectionLifetime pour éviter les problèmes de DNS obsolète.

Cookies

Le regroupement des instances HttpMessageHandler engendre le partage d’objets CookieContainer. Le partage d’objets CookieContainer imprévus entraîne souvent un code incorrect. Pour les applications qui nécessitent des cookie, tenez compte de ce qui suit :

  • Désactivation de la gestion de cookie automatique
  • Éviter IHttpClientFactory

Appelez ConfigurePrimaryHttpMessageHandler pour désactiver la gestion de cookie automatique :

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Journalisation

Les clients créés via IHttpClientFactory enregistrent les messages de journalisation pour toutes les requêtes. Activez le niveau d’informations approprié dans la configuration de journalisation pour voir les messages de journalisation par défaut. Une journalisation supplémentaire, comme celle des en-têtes des requêtes, est incluse seulement au niveau de trace.

La catégorie de journal utilisée pour chaque client comprend le nom du client. Par exemple, un client nommé MyNamedClient journalise les messages avec la catégorie « System.Net.Http.HttpClient.MyNamedClient.LogicalHandler ». Les messages avec le suffixe LogicalHandler se produisent en dehors du pipeline du gestionnaire de requêtes. Lors d’une requête, les messages sont journalisés avant que d’autres gestionnaires du pipeline l’aient traitée. Lors d’une réponse, les messages sont journalisés une fois que tous les autres gestionnaires du pipeline ont reçu la réponse.

La journalisation se produit également à l’intérieur du pipeline du gestionnaire de requêtes. Dans l’exemple MyNamedClient, ces messages sont enregistrés avec la catégorie de journal « System.Net.Http.HttpClient.MyNamedClient.ClientHandler ». Pour la requête, cela se produit après que tous les autres gestionnaires ont été exécutés et immédiatement avant l’envoi de la requête. Lors de la réponse, cette journalisation inclut l’état de la réponse avant qu’elle repasse à travers le pipeline de gestionnaires.

L’activation de la journalisation à l’extérieur et à l’intérieur du pipeline permet l’inspection des changements apportés par les autres gestionnaires du pipeline. Cela peut comprendre des changements apportés aux en-têtes des requêtes ou au code d’état de la réponse.

L’ajout du nom du client dans la catégorie de journalisation permet de filtrer le journal pour des clients nommés spécifiques.

Configurer le HttpMessageHandler

Il peut être nécessaire de contrôler la configuration du HttpMessageHandler interne utilisé par un client.

Un IHttpClientBuilder est retourné quand vous ajoutez des clients nommés ou typés. La méthode d’extension ConfigurePrimaryHttpMessageHandler peut être utilisée pour définir un délégué. Le délégué est utilisé pour créer et configurer le HttpMessageHandler principal utilisé par ce client :

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

Utiliser IHttpClientFactory dans une application console

Dans une application console, ajoutez les références de package suivantes au projet :

Dans l’exemple suivant :

  • IHttpClientFactory est inscrit dans le conteneur de service dans de l’hôte générique.
  • MyService crée une instance de fabrique cliente à partir du service, qui est utilisée pour créer un HttpClient. HttpClient est utilisé pour récupérer une page web.
  • Main crée une étendue pour exécuter la méthode GetPage du service et écrire les 500 premiers caractères du contenu de la page web dans la console.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

Intergiciel de propagation d’en-tête

La propagation d’en-tête est un intergiciel ASP.NET Core pour propager les en-têtes HTTP de la requête entrante vers les requêtes client HTTP sortantes. Pour utiliser la propagation d’en-tête :

  • Référencez le package Microsoft.AspNetCore.HeaderPropagation.

  • Configurez l’intergiciel et HttpClient dans Startup :

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseRouting();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  • Le client inclut les en-têtes configurés sur les requêtes sortantes :

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Ressources supplémentaires

By Glenn Condron, Ryan Nowak et Steve Gordon

Une IHttpClientFactory peut être inscrite et utilisée pour configurer et créer des instances de HttpClient dans une application. Elle offre les avantages suivants :

  • Fournit un emplacement central pour le nommage et la configuration d’instance de HttpClient logiques. Par exemple, un client github peut être inscrit et configuré pour accéder à GitHub. Un client par défaut peut être inscrit à d’autres fins.
  • Codifie le concept de middleware (intergiciel) sortant via la délégation de gestionnaires dans HttpClient et fournit des extensions permettant au middleware Polly d’en tirer parti.
  • Gère le regroupement et la durée de vie des instances de HttpClientMessageHandler sous-jacentes pour éviter les problèmes DNS courants qui se produisent lors de la gestion manuelle des durées de vie de HttpClient.
  • Ajoute une expérience de journalisation configurable (via ILogger) pour toutes les requêtes envoyées via des clients créés par la fabrique.

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Prérequis

Les projets ciblant .NET Framework nécessitent l’installation du package NuGet Microsoft.Extensions.Http. Les projets qui ciblent .NET Core et référencent le métapackage Microsoft.AspNetCore.App incluent déjà le package Microsoft.Extensions.Http.

Modèles de consommation

Vous pouvez utiliser IHttpClientFactory dans une application de plusieurs façons :

Aucune d’entre elles n’est meilleure qu’une autre. La meilleure approche dépend des contraintes de l’application.

Utilisation de base

Vous pouvez inscrire la IHttpClientFactory en appelant la méthode d’extension AddHttpClient sur la IServiceCollection, à l’intérieur la méthode Startup.ConfigureServices.

services.AddHttpClient();

Une fois inscrit, le code peut accepter un IHttpClientFactory partout où des services peuvent être injectés avec une injection de dépendance. La IHttpClientFactory peut être utilisée pour créer une instance de HttpClient :

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            Branches = await response.Content
                .ReadAsAsync<IEnumerable<GitHubBranch>>();
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }                               
    }
}

L’utilisation de IHttpClientFactory de cette façon est un excellent moyen de refactoriser une application existante. Elle n’a aucun impact sur la façon dont HttpClient est utilisé. Dans les endroits où les instances de HttpClient sont actuellement créées, remplacez ces occurrences par un appel à CreateClient.

Clients nommés

Si une application nécessite plusieurs utilisations distinctes de HttpClient, chacune avec une configuration différente, une option consiste à utiliser des clients nommés. La configuration d’un HttpClient nommé peut être spécifiée lors de l’inscription dans Startup.ConfigureServices.

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

Dans le code précédent, AddHttpClient est appelé en fournissant le nom github. Une configuration par défaut est appliquée à ce client : l’adresse de base et deux en-têtes nécessaires pour utiliser l’API GitHub.

Chaque fois que CreateClient est appelée, une nouvelle instance de HttpClient est créée et l’action de configuration est appelée.

Pour utiliser un client nommé, un paramètre de chaîne peut être passé à CreateClient. Spécifiez le nom du client à créer :

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            PullRequests = await response.Content
                .ReadAsAsync<IEnumerable<GitHubPullRequest>>();
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

Dans le code précédent, la requête n’a pas besoin de spécifier un nom d’hôte. Elle peut simplement passer le chemin, car l’adresse de base configurée pour le client est utilisée.

Clients typés

Clients typés :

  • Fournissent les mêmes fonctionnalités que les clients nommés, sans qu’il soit nécessaire d’utiliser des chaînes comme clés.
  • Bénéficie de l’aide d’IntelliSense et du compilateur lors de l’utilisation des clients.
  • Fournissent un emplacement unique pour configurer et interagir avec un HttpClient particulier. Par exemple, un même client typé peut être utilisé pour un point de terminaison de backend et pour encapsuler la logique relative à ce point de terminaison.
  • Fonctionnent avec l’injection de dépendances et peuvent être injectés là où c’est nécessaire dans votre application.

Un client typé accepte un paramètre HttpClient dans son constructeur :

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept", 
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent", 
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<GitHubIssue>>();

        return result;
    }
}

Dans le code précédent, la configuration est déplacée dans le client typé. L’objet HttpClient est exposé en tant que propriété publique. Il est possible de définir des méthodes d’API spécifiques qui exposent les fonctionnalités de HttpClient. La méthode GetAspNetDocsIssues encapsule le code nécessaire pour interroger et analyser les problèmes ouverts les plus récents d’un dépôt GitHub.

Pour inscrire un client typé, la méthode d’extension AddHttpClient générique peut être utilisée dans Startup.ConfigureServices, en spécifiant la classe du client typé :

services.AddHttpClient<GitHubService>();

Le client typé est inscrit comme étant transitoire avec injection de dépendances. Le client typé peut être injecté et utilisé directement :

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

Si vous préférez, vous pouvez spécifier la configuration d’un client typé lors de l’inscription dans Startup.ConfigureServices au lieu de le faire dans le constructeur du client typé :

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

Il est possible d’encapsuler entièrement le HttpClient dans un client typé. Au lieu de l’exposer en tant que propriété, vous pouvez fournir des méthodes publiques qui appellent l’instance de HttpClient en interne.

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<string>>();

        return result;
    }
}

Dans le code précédent, le HttpClient est stocké en tant que champ privé. Tous les accès nécessaires pour effectuer des appels externes passent par la méthode GetRepos.

Clients générés

IHttpClientFactory peut être utilisé en combinaison avec d’autres bibliothèques tierces, comme Refit. Refit est une bibliothèque REST pour .NET. Il convertit les API REST en interfaces dynamiques. Une implémentation de l’interface est générée dynamiquement par le RestService, avec HttpClient pour faire les appels HTTP externes.

Une interface et une réponse sont définies pour représenter l’API externe et sa réponse :

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

Vous pouvez ajouter un client typé en utilisant Refit pour générer l’implémentation :

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddMvc();
}

L’interface définie peut être utilisée quand c’est nécessaire, avec l’implémentation fournie par l’injection de dépendances et Refit :

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

Middleware pour les requêtes sortantes

HttpClient intègre déjà le concept de délégation des gestionnaires qui peuvent être liés ensemble pour les requêtes HTTP sortantes. Le IHttpClientFactory permet de définir facilement les gestionnaires à appliquer pour chaque client nommé. Il prend en charge l’inscription et le chaînage de plusieurs gestionnaires pour créer un pipeline de middlewares pour les requêtes sortantes. Chacun de ces gestionnaires peut effectuer un travail avant et après la requête sortante. Ce modèle est similaire au pipeline de middlewares entrants dans ASP.NET Core. Le modèle fournit un mécanisme permettant de gérer les problèmes transversaux liés aux des requêtes HTTP, notamment la mise en cache, la gestion des erreurs, la sérialisation et la journalisation.

Pour créer un gestionnaire, définissez une classe dérivant de DelegatingHandler. Remplacez la méthode SendAsync de façon à exécuter du code avant de passer la requête au gestionnaire suivant dans le pipeline :

public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Le code précédent définit un gestionnaire de base. Il vérifie si un en-tête X-API-KEY a été inclus dans la requête. Si l’en-tête est manquant, il peut éviter l’appel HTTP et retourner une réponse appropriée.

Lors de l’inscription, un ou plusieurs gestionnaires peuvent être ajoutés à la configuration pour un HttpClient. Cette tâche est accomplie via des méthodes d’extension sur le IHttpClientBuilder.

services.AddTransient<ValidateHeaderHandler>();

services.AddHttpClient("externalservice", c =>
{
    // Assume this is an "external" service which requires an API KEY
    c.BaseAddress = new Uri("https://localhost:5000/");
})
.AddHttpMessageHandler<ValidateHeaderHandler>();

Dans le code précédent, le ValidateHeaderHandler est inscrit avec une injection de dépendances. Le gestionnaire doit être inscrit dans l’injection de dépendances en tant que service temporaire, sans étendue. Si le gestionnaire est inscrit en tant que service étendu et que des services dont dépend le gestionnaire peuvent être supprimés :

  • Les services du gestionnaire peuvent être supprimés avant que le gestionnaire ne soit hors de portée.
  • Les services du gestionnaire supprimés entraînent un échec du gestionnaire.

Une fois inscrit, AddHttpMessageHandler peut être appelé en passant en entrée le type du gestionnaire.

Vous pouvez inscrire plusieurs gestionnaires dans l’ordre où ils doivent être exécutés. Chaque gestionnaire wrappe le gestionnaire suivant jusqu’à ce que le dernier HttpClientHandler exécute la requête :

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Utilisez l’une des approches suivantes pour partager l’état de chaque requête avec les gestionnaires de messages :

  • Passez des données dans le gestionnaire en utilisant HttpRequestMessage.Properties.
  • Utilisez IHttpContextAccessor pour accéder à la requête en cours.
  • Créez un objet de stockage AsyncLocal personnalisé pour passer les données.

Utiliser les gestionnaires Polly

IHttpClientFactory s’intègre à une bibliothèque tierce très utilisée, appelée Polly. Polly est une bibliothèque complète de gestion des erreurs transitoires et de résilience pour .NET. Elle permet aux développeurs de formuler facilement et de façon thread-safe des stratégies, comme Retry (Nouvelle tentative), Circuit Breaker (Disjoncteur), Timeout (Délai d’attente), Bulkhead Isolation (Isolation par cloisonnement) et Fallback (Alternative de repli).

Des méthodes d’extension sont fournies pour permettre l’utilisation de stratégies Polly avec les instances configurées de HttpClient. Les extensions Polly :

  • Prennent en charge l’ajout de gestionnaires Polly à des clients.
  • Peuvent être utilisées après l’installation du package NuGet Microsoft.Extensions.Http.Polly. Ce package n’est pas inclus dans le framework partagé ASP.NET Core.

Gérer les erreurs temporaires

Les erreurs courantes se produisent lorsque des appels HTTP externes sont temporaires. Une méthode d’extension pratique nommée AddTransientHttpErrorPolicy est incluse : elle permet de définir une stratégie pour gérer les erreurs temporaires. Les stratégies configurées avec cette méthode d’extension gèrent HttpRequestException, les réponses HTTP 5xx et les réponses HTTP 408.

L’extension AddTransientHttpErrorPolicy peut être utilisée dans Startup.ConfigureServices. L’extension fournit l’accès à un objet PolicyBuilder configuré pour gérer les erreurs représentant une erreur temporaire possible :

services.AddHttpClient<UnreliableEndpointCallerService>()
    .AddTransientHttpErrorPolicy(p => 
        p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

Dans le code précédent, une stratégie WaitAndRetryAsync est définie. Les requêtes qui ont échoué sont retentées jusqu’à trois fois avec un délai de 600 ms entre les tentatives.

Sélectionner dynamiquement des stratégies

Il existe d’autres méthodes d’extension que vous pouvez utiliser pour ajouter des gestionnaires Polly. Une de ces extensions est AddPolicyHandler, qui a plusieurs surcharges. Une de ces surcharges permet l’inspection de la requête lors de la définition de la stratégie à appliquer :

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

Dans le code précédent, si la requête sortante est un HTTP GET, un délai d’attente de 10 secondes est appliqué. Pour toutes les autres méthodes HTTP, un délai d’attente de 30 secondes est utilisé.

Ajouter plusieurs gestionnaires Polly

Il est courant d’imbriquer des stratégies Polly pour fournir des fonctionnalités améliorées :

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

Dans l’exemple précédent, deux gestionnaires sont ajoutés. Le premier utilise l’extension AddTransientHttpErrorPolicy pour ajouter une stratégie de nouvelle tentative. Les requêtes qui ont échoué sont retentées jusqu’à trois fois. Le deuxième appel à AddTransientHttpErrorPolicy ajoute une stratégie de disjoncteur. Les requêtes externes supplémentaires sont bloquées pendant 30 secondes si cinq tentatives successives échouent. Les stratégies de disjoncteur sont avec état. Tous les appels effectués via ce client partagent le même état du circuit.

Ajouter des stratégies à partir du Registre Polly

Une approche de la gestion des stratégies régulièrement utilisées consiste à les définir une seule fois et à les inscrire avec un PolicyRegistry. Il existe une méthode d’extension qui permet l’ajout d’un gestionnaire avec une stratégie du Registre :

var registry = services.AddPolicyRegistry();

registry.Add("regular", timeout);
registry.Add("long", longTimeout);

services.AddHttpClient("regulartimeouthandler")
    .AddPolicyHandlerFromRegistry("regular");

Dans le code précédent, deux stratégies sont inscrites lorsque PolicyRegistry est ajouté à ServiceCollection. Pour utiliser une stratégie du Registre, la méthode AddPolicyHandlerFromRegistry est utilisée, en passant le nom de la stratégie à appliquer.

Vous trouverez plus d’informations sur les intégrations de IHttpClientFactory et de Polly dans le wiki Polly.

HttpClient et gestion de la durée de vie

Une nouvelle instance HttpClient est retournée à chaque fois que CreateClient est appelé sur IHttpClientFactory. Il existe un HttpMessageHandler par client nommé. La fabrique gère les durées de vie des instances HttpMessageHandler.

IHttpClientFactory regroupe dans un pool les instances de HttpMessageHandler créées par la fabrique pour réduire la consommation des ressources. Une instance de HttpMessageHandler peut être réutilisée à partir du pool quand vous créez une instance de HttpClient si sa durée de vie n’a pas expiré.

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

La durée de vie par défaut d’un gestionnaire est de deux minutes. La valeur par défaut peut être remplacée pour chaque client nommé. Pour la remplacer, appelez SetHandlerLifetime sur le IHttpClientBuilder qui est retourné lors de la création du client :

services.AddHttpClient("extendedhandlerlifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

La suppression du client n’est pas nécessaire. La suppression annule les requêtes sortantes et garantit que l’instance HttpClient donnée ne peut pas être utilisée après avoir appelé Dispose. IHttpClientFactory effectue le suivi et libère les ressources utilisées par les instances HttpClient. Les instances HttpClient peuvent généralement être traitées en tant qu’objets .NET ne nécessitant pas une suppression.

Le fait de conserver une seule instance HttpClient active pendant une longue durée est un modèle commun utilisé avant le lancement de IHttpClientFactory. Ce modèle devient inutile après la migration vers IHttpClientFactory.

Alternatives à IHttpClientFactory

L’utilisation de IHttpClientFactory dans une application avec injection de dépendances évite :

  • Les problèmes d’épuisement des ressources en regroupant les instances HttpMessageHandler.
  • Les problèmes de DNS obsolète en permutant les instances HttpMessageHandler à intervalles réguliers.

Il existe d’autres façons de résoudre les problèmes précédents à l’aide d’une instance SocketsHttpHandler de longue durée.

  • Créez une instance de SocketsHttpHandler lorsque l’application démarre et utilisez-la pour la durée de vie de l’application.
  • Configurez PooledConnectionLifetime sur une valeur appropriée en fonction des temps d’actualisation DNS.
  • Créez des instances HttpClient à l’aide de new HttpClient(handler, disposeHandler: false) si nécessaire.

Les approches précédentes résolvent les problèmes de gestion des ressources que IHttpClientFactory résout de la même manière.

  • Le SocketsHttpHandler partage les connexions entre les instances HttpClient. Ce partage empêche l’épuisement des sockets.
  • Le SocketsHttpHandler permute les connexions en fonction de PooledConnectionLifetime pour éviter les problèmes de DNS obsolète.

Cookies

Le regroupement des instances HttpMessageHandler engendre le partage d’objets CookieContainer. Le partage d’objets CookieContainer imprévus entraîne souvent un code incorrect. Pour les applications qui nécessitent des cookie, tenez compte de ce qui suit :

  • Désactivation de la gestion de cookie automatique
  • Éviter IHttpClientFactory

Appelez ConfigurePrimaryHttpMessageHandler pour désactiver la gestion de cookie automatique :

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Journalisation

Les clients créés via IHttpClientFactory enregistrent les messages de journalisation pour toutes les requêtes. Activez le niveau d’informations approprié dans votre configuration de journalisation pour voir les messages de journalisation par défaut. Une journalisation supplémentaire, comme celle des en-têtes des requêtes, est incluse seulement au niveau de trace.

La catégorie de journal utilisée pour chaque client comprend le nom du client. Par exemple, un client nommé MyNamedClient journalise les messages avec la catégorie System.Net.Http.HttpClient.MyNamedClient.LogicalHandler. Les messages avec le suffixe LogicalHandler se produisent en dehors du pipeline du gestionnaire de requêtes. Lors d’une requête, les messages sont journalisés avant que d’autres gestionnaires du pipeline l’aient traitée. Lors d’une réponse, les messages sont journalisés une fois que tous les autres gestionnaires du pipeline ont reçu la réponse.

La journalisation se produit également à l’intérieur du pipeline du gestionnaire de requêtes. Dans l’exemple MyNamedClient, ces messages sont journalisés avec la catégorie de journalisation System.Net.Http.HttpClient.MyNamedClient.ClientHandler. Pour la requête, cela se produit après que tous les autres gestionnaires ont été exécutés et immédiatement avant l’envoi de la requête sur le réseau. Lors de la réponse, cette journalisation inclut l’état de la réponse avant qu’elle repasse à travers le pipeline de gestionnaires.

L’activation de la journalisation à l’extérieur et à l’intérieur du pipeline permet l’inspection des changements apportés par les autres gestionnaires du pipeline. Par exemple, cela peut comprendre des changements apportés aux en-têtes des requêtes ou au code d’état de la réponse.

L’ajout du nom du client dans la catégorie de journalisation permet si nécessaire de filtrer le journal pour des clients nommés spécifiques.

Configurer le HttpMessageHandler

Il peut être nécessaire de contrôler la configuration du HttpMessageHandler interne utilisé par un client.

Un IHttpClientBuilder est retourné quand vous ajoutez des clients nommés ou typés. La méthode d’extension ConfigurePrimaryHttpMessageHandler peut être utilisée pour définir un délégué. Le délégué est utilisé pour créer et configurer le HttpMessageHandler principal utilisé par ce client :

services.AddHttpClient("configured-inner-handler")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            AllowAutoRedirect = false,
            UseDefaultCredentials = true
        };
    });

Utiliser IHttpClientFactory dans une application console

Dans une application console, ajoutez les références de package suivantes au projet :

Dans l’exemple suivant :

  • IHttpClientFactory est inscrit dans le conteneur de service dans de l’hôte générique.
  • MyService crée une instance de fabrique cliente à partir du service, qui est utilisée pour créer un HttpClient. HttpClient est utilisé pour récupérer une page web.
  • La méthode GetPage du service est exécutée pour écrire les 500 premiers caractères du contenu de la page web dans la console. Pour plus d’informations sur l’appel de services à partir de Program.Main, consultez Injection de dépendances dans ASP.NET Core.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

Intergiciel de propagation d’en-tête

La propagation d’en-tête est un intergiciel pris en charge par la communauté pour propager les en-têtes HTTP de la requête entrante vers les requêtes client HTTP sortantes. Pour utiliser la propagation d’en-tête :

  • Référencez le portage pris en charge par la communauté du package HeaderPropagation. ASP.NET Core 3.1 et versions ultérieures prennent en charge Microsoft.AspNetCore.HeaderPropagation.

  • Configurez l’intergiciel et HttpClient dans Startup :

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseMvc();
    }
    
  • Le client inclut les en-têtes configurés sur les requêtes sortantes :

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Ressources supplémentaires