IHttpClientFactory con .NET

In questo articolo apprenderai come usare IHttpClientFactory per creare tipi HttpClient con diversi concetti fondamentali di .NET, ad esempio inserimento delle dipendenze (DI), registrazione e configurazione. Il tipo HttpClient è stato introdotto in .NET Framework 4.5, rilasciato nel 2012. In altre parole, è stato attivo per un po'. HttpClient viene usato per effettuare richieste HTTP e gestire le risposte HTTP dalle risorse Web identificate da un oggetto Uri. Il protocollo HTTP gestisce la maggior parte del traffico Internet.

Con i moderni principi di sviluppo delle applicazioni che guidano le procedure consigliate, IHttpClientFactory funge da astrazione factory che può creare istanze HttpClient con configurazioni personalizzate. IHttpClientFactory è stato introdotto in .NET Core 2.1. I carichi di lavoro .NET basati su HTTP comuni possono sfruttare facilmente il middleware di terze parti resiliente e in grado di gestire gli errori temporanei.

Nota

Se l'app richiede cookie, potrebbe essere preferibile evitare di usare IHttpClientFactory nell'app. Per modi alternativi di gestione dei client, vedi Linee guida per l'uso dei client HTTP.

Importante

La gestione della durata delle istanze HttpClient create da IHttpClientFactory è completamente diversa dalle istanze create manualmente. Le strategie sono l'uso di client di breve durata creati da IHttpClientFactory o client di lunga durata con configurazione PooledConnectionLifetime. Per altre informazioni, vedi la sezione Gestione della durata httpClient e Linee guida per l'uso dei client HTTP.

Il tipo IHttpClientFactory

Tutto il codice sorgente di esempio in questo articolo si basa sul pacchetto NuGet Microsoft.Extensions.Http. Inoltre, le richieste HTTP GET vengono inviate all'API segnaposto {JSON} gratuita per ottenere gli oggetti utente Todo.

Quando chiami uno dei metodi di estensione AddHttpClient, aggiungi IHttpClientFactory e i servizi correlati a IServiceCollection. Il tipo IHttpClientFactory offre i vantaggi seguenti:

  • Espone la classe HttpClient come tipo pronto per l'inserimento delle dipendenze.
  • Offre una posizione centrale per la denominazione e la configurazione di istanze di HttpClient logiche.
  • Codifica il concetto di middleware in uscita tramite delega dei gestori in HttpClient.
  • Fornisce metodi di estensione per il middleware basato su Polly per sfruttare i vantaggi della delega dei gestori in HttpClient.
  • Gestisce la memorizzazione nella cache e la durata delle istanze HttpClientHandler sottostanti. La gestione automatica evita problemi comuni del sistema DNS (Domain Name System) che si verificano durante la gestione manuale delle durate HttpClient.
  • Aggiunge un'esperienza di registrazione configurabile, tramite ILogger, per tutte le richieste inviate attraverso i client creati dalla factory.

Modelli di consumo

IHttpClientFactory può essere usato in un'app in diversi modi:

L'approccio migliore dipende dai requisiti dell'app.

Utilizzo di base

Per registrare IHttpClientFactory, chiama AddHttpClient:

using Shared;
using BasicHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient();
builder.Services.AddTransient<TodoService>();

using IHost host = builder.Build();

L'utilizzo di servizi può richiedere IHttpClientFactory come parametro del costruttore con inserimento di dipendenze. Il codice seguente usa IHttpClientFactory per creare un'istanza di HttpClient:

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Shared;

namespace BasicHttp.Example;

public sealed class TodoService(
    IHttpClientFactory httpClientFactory,
    ILogger<TodoService> logger)
{
    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        // Create the client
        using HttpClient client = httpClientFactory.CreateClient();
        
        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo types
            Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
                $"https://jsonplaceholder.typicode.com/todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }
}

L'uso di IHttpClientFactory come nell'esempio precedente è un buon modo per effettuare il refactoring di un'app esistente. Non ha alcun impatto sulla modalità di utilizzo di HttpClient. Nelle posizioni in cui le istanze HttpClient vengono create in un'app esistente, sostituisci tali occorrenze con chiamate a CreateClient.

Client denominati

I client nominati sono una scelta ottimale quando:

  • L'app richiede molti usi distinti di HttpClient.
  • Molte istanze HttpClient hanno configurazioni diverse.

La configurazione di un HttpClient denominato può essere specificata durante la registrazione su IServiceCollection:

using Shared;
using NamedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

string? httpClientName = builder.Configuration["TodoHttpClientName"];
ArgumentException.ThrowIfNullOrEmpty(httpClientName);

builder.Services.AddHttpClient(
    httpClientName,
    client =>
    {
        // Set the base address of the named client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

Nel codice precedente il client è configurato con:

  • Nome estratto dalla configurazione in "TodoHttpClientName".
  • Indirizzo di base https://jsonplaceholder.typicode.com/.
  • Intestazione "User-Agent".

Puoi usare la configurazione per specificare i nomi dei client HTTP, utile per evitare la denominazione errata dei client durante l'aggiunta e la creazione. In questo esempio viene usato il file appsettings.json per configurare il nome del client HTTP:

{
    "TodoHttpClientName": "JsonPlaceholderApi"
}

È facile estendere questa configurazione e archiviare altri dettagli sulla modalità di funzionamento del client HTTP. Per altre informazioni, vedi Configurazione in .NET.

Creare il client

Ogni volta che viene chiamato CreateClient:

  • Viene creata una nuova istanza di HttpClient.
  • Viene chiamata l'azione di configurazione.

Per creare un client denominato, passarne il nome in CreateClient:

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Shared;

namespace NamedHttp.Example;

public sealed class TodoService
{
    private readonly IHttpClientFactory _httpClientFactory = null!;
    private readonly IConfiguration _configuration = null!;
    private readonly ILogger<TodoService> _logger = null!;

    public TodoService(
        IHttpClientFactory httpClientFactory,
        IConfiguration configuration,
        ILogger<TodoService> logger) =>
        (_httpClientFactory, _configuration, _logger) =
            (httpClientFactory, configuration, logger);

    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        // Create the client
        string? httpClientName = _configuration["TodoHttpClientName"];
        using HttpClient client = _httpClientFactory.CreateClient(httpClientName ?? "");

        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo type
            Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
                $"todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            _logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }
}

Nel codice precedente non è necessario che la richiesta specifichi un nome host. Poiché viene usato l'indirizzo di base configurato per il client, il codice può passare solo il percorso.

Client tipizzati

Client tipizzati:

  • Offrono le stesse funzionalità dei client denominati senza la necessità di usare le stringhe come chiavi.
  • Offrire l'aiuto di IntelliSense e del compilatore quando si usano i client.
  • La configurazione e l'interazione con un particolare HttpClient può avvenire in un'unica posizione. Ad esempio, è possibile usare un singolo client tipizzato:
    • Per un singolo endpoint back-end.
    • Per incapsulare tutta la logica che riguarda l'endpoint.
  • Usano l'inserimento di dipendenze e possono essere inseriti dove necessario nell'app.

Un client tipizzato accetta il parametro HttpClient nel proprio costruttore:

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Shared;

namespace TypedHttp.Example;

public sealed class TodoService(
    HttpClient httpClient,
    ILogger<TodoService> logger) : IDisposable
{
    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo type
            Todo[]? todos = await httpClient.GetFromJsonAsync<Todo[]>(
                $"todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }

    public void Dispose() => httpClient?.Dispose();
}

Nel codice precedente:

  • La configurazione viene impostata quando il client tipizzato viene aggiunto alla raccolta di servizi.
  • HttpClient viene assegnato come variabile in ambito di classe (campo) e usato con le API esposte.

È possibile creare metodi specifici dell'API che espongono funzionalità HttpClient. Ad esempio, il metodo GetUserTodosAsync incapsula il codice per recuperare oggetti Todo specifici dell'utente.

Il codice seguente chiama AddHttpClient per registrare una classe client tipizzata:

using Shared;
using TypedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient<TodoService>(
    client =>
    {
        // Set the base address of the typed client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

Il client tipizzato viene registrato come temporaneo nell'inserimento di dipendenze. Nel codice precedente AddHttpClient registra TodoService come servizio temporaneo. Questa registrazione usa un metodo factory per:

  1. Creare un'istanza di HttpClient.
  2. Creare un'istanza di TodoService, passando l'istanza di HttpClient al relativo costruttore.

Importante

L'uso di client tipizzati nei servizi singleton può essere pericoloso. Per altre informazioni, vedi la sezione Evitare client tipizzati nei servizi singleton.

Nota

Quando si registra un client tipizzato con il metodo AddHttpClient<TClient>, il tipo TClient deve avere un costruttore che accetta un parametro HttpClient. Inoltre, il tipo TClient non deve essere registrato separatamente con il contenitore di inserimento delle dipendenze.

Client denominati e tipizzati

I client denominati e i client tipizzati hanno propri punti di forza e punti deboli. Esiste un modo per combinare questi due tipi di client per ottenere il meglio di entrambi.

Il caso d'uso principale è il seguente: usare lo stesso client tipizzato ma in domini diversi. Ad esempio, si dispone di un servizio primario e secondario ed espongono esattamente la stessa funzionalità. Ciò significa che è possibile usare lo stesso client tipizzato per eseguire il wrapping dell'utilizzo HttpClient per inviare richieste, elaborare risposte e gestire errori. Verrà usato esattamente lo stesso codice, ma con configurazioni diverse (ad esempio indirizzo di base, timeout e credenziali diverse).

Nell'esempio seguente viene usato lo stesso client tipizzato TodoService visualizzato nella sezione client tipizzata.

Registrare prima di tutto i client denominati e tipizzati.

using Shared;
using TypedHttp.Example;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient<TodoService>("primary"
    client =>
    {
        // Configure the primary typed client
        client.BaseAddress = new Uri("https://primary-host-address.com/");
        client.Timeout = TimeSpan.FromSeconds(3);
    });

// Register the same typed client but with different settings
builder.Services.AddHttpClient<TodoService>("secondary"
    client =>
    {
        // Configure the secondary typed client
        client.BaseAddress = new Uri("https://secondary-host-address.com/");
        client.Timeout = TimeSpan.FromSeconds(10);
    });

Nel codice precedente:

  • La prima AddHttpClientchiamata registra un TodoServiceclient tipizzato sotto il nome primary. Il HttpClient sottostante punta al servizio primario e presenta un breve timeout.
  • La seconda AddHttpClientchiamata registra un TodoService client tipizzato sotto il nome secondary. Il HttpClient sottostante punta al servizio secondario e presenta un timeout più lungo.
using IHost host = builder.Build();

// Fetch an IHttpClientFactory instance to create a named client
IHttpClientFactory namedClientFactory =
    host.Services.GetRequiredService<IHttpClientFactory>();

// Fetch an ITypedHttpClientFactory<TodoService> instance to create a named and typed client
ITypedHttpClientFactory<TodoService> typedClientFactory  =
    host.Services.GetRequiredService<ITypedHttpClientFactory<TodoService>>();

// Create a TodoService instance against the primary host
var primaryClient = namedClientFactory.CreateClient("primary");
var todoService = typedClientFactory.CreateClient(primaryClient);

Nel codice precedente:

  • Un'istanza di IHttpClientFactory viene recuperata dal contenitore di inserimento delle dipendenze per poter creare client denominati tramite il relativo metodo CreateClient.
  • Un'istanza di ITypedHttpClientFactory<TodoService> viene recuperata dal contenitore di inserimento delle dipendenze per poter creare client tipizzati tramite il relativo metodo CreateClient.
    • Questo overload CreateClient ha ricevuto un HttpClient denominato (con la configurazione corretta) come parametro.
    • Il todoService creato è configurato per l'uso del servizio primario.

Nota

Il tipo IHttpClientFactory si trova all'interno degli spazi dei nomi System.Net.Http, mentre il tipo ITypedHttpClientFactory all'interno del Microsoft.Extensions.Http.

Importante

Usare la classe di implementazione (nell'esempio precedente il TodoService) come parametro di tipo per il ITypedHttpClientFactory. Anche se si dispone di un'astrazione (ad esempio interfaccia ITodoService), è comunque necessario usare l'implementazione. Se si usa accidentalmente l'astrazione (ITodoService), quando si chiama il relativo CreateClient genererà un InvalidOperationException.

try
{
    Todo[] todos = await todoService.GetUserTodosAsync(4);
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
    // The request timed out against the primary host

    // Create a TodoService instance against the secondary host
    var fallbackClient = namedClientFactory.CreateClient("secondary");
    var todoFallbackService = typedClientFactory.CreateClient(fallbackClient);

    // Issue request against the secondary host
    Todo[] todos = await todoFallbackService.GetUserTodosAsync(4);
}

Nel codice precedente:

  • Tenta di emettere una richiesta per il servizio primario.
  • Se la richiesta raggiunge il timeout (richiede più di 3 secondi), genera un TaskCanceledException con un TimeoutException interno.
  • In caso di timeout, viene creato e usato un nuovo client che ora è destinato al servizio secondario.

Client generati

È possibile usare IHttpClientFactory in combinazione con librerie di terze parti, ad esempio Refit. Refit è una libreria REST per .NET. Consente le definizioni dichiarative dell'API REST, il mapping dei metodi di interfaccia agli endpoint. Un'implementazione dell'interfaccia viene generata dinamicamente da RestService, usando HttpClient per effettuare le chiamate HTTP esterne.

Considera il tipo record seguente:

namespace Shared;

public record class Todo(
    int UserId,
    int Id,
    string Title,
    bool Completed);

L'esempio seguente si basa sul pacchetto NuGet Refit.HttpClientFactory ed è un'interfaccia semplice:

using Refit;
using Shared;

namespace GeneratedHttp.Example;

public interface ITodoService
{
    [Get("/todos?userId={userId}")]
    Task<Todo[]> GetUserTodosAsync(int userId);
}

L’interfaccia C# precedente:

  • Definisce un metodo denominato GetUserTodosAsync che restituisce un'istanza di Task<Todo[]>.
  • Dichiara un attributo Refit.GetAttribute con il percorso e la stringa di query nell'API esterna.

È possibile aggiungere un client tipizzato usando Refit per generare l'implementazione:

using GeneratedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Refit;
using Shared;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddRefitClient<ITodoService>()
    .ConfigureHttpClient(client =>
    {
        // Set the base address of the named client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

L'interfaccia definita può essere usata dove necessario, con l'implementazione offerta dall'inserimento di dipendenze e da Refit.

Effettuare richieste POST, PUT e DELETE

Negli esempi precedenti tutte le richieste HTTP usano il verbo HTTP GET. HttpClient supporta anche altri verbi HTTP, tra cui:

  • POST
  • PUT
  • DELETE
  • PATCH

Per un elenco completo dei verbi HTTP supportati, vedi HttpMethod. Per altre informazioni sull'esecuzione di richieste HTTP, vedi Inviare una richiesta tramite HttpClient.

L'esempio seguente illustra come effettuare una richiesta HTTP POST:

public async Task CreateItemAsync(Item item)
{
    using StringContent json = new(
        JsonSerializer.Serialize(item, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
        Encoding.UTF8,
        MediaTypeNames.Application.Json);

    using HttpResponseMessage httpResponse =
        await httpClient.PostAsync("/api/items", json);

    httpResponse.EnsureSuccessStatusCode();
}

Nel codice precedente il metodo CreateItemAsync:

  • Serializza il parametro Item in JSON usando System.Text.Json. Usa un'istanza di JsonSerializerOptions per configurare il processo di serializzazione.
  • Crea un'istanza di StringContent per creare un pacchetto del codice JSON serializzato per l'invio nel corpo della richiesta HTTP.
  • Chiama PostAsync per inviare il contenuto JSON all'URL specificato. Si tratta di un URL relativo che viene aggiunto a HttpClient.BaseAddress.
  • Chiama EnsureSuccessStatusCode per generare un'eccezione se il codice di stato della risposta non indica l’esito positivo.

HttpClient supporta anche altri tipi di contenuto. Ad esempio, MultipartContent e StreamContent. Per un elenco completo del contenuto supportato, vedi HttpContent.

L'esempio seguente mostra una richiesta HTTP PUT:

public async Task UpdateItemAsync(Item item)
{
    using StringContent json = new(
        JsonSerializer.Serialize(item, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
        Encoding.UTF8,
        MediaTypeNames.Application.Json);

    using HttpResponseMessage httpResponse =
        await httpClient.PutAsync($"/api/items/{item.Id}", json);

    httpResponse.EnsureSuccessStatusCode();
}

Il codice precedente è molto simile all'esempio POST. Il metodo UpdateItemAsync chiama PutAsync anziché PostAsync.

L'esempio seguente mostra una richiesta HTTP DELETE:

public async Task DeleteItemAsync(Guid id)
{
    using HttpResponseMessage httpResponse =
        await httpClient.DeleteAsync($"/api/items/{id}");

    httpResponse.EnsureSuccessStatusCode();
}

Nel codice precedente il metodo DeleteItemAsync chiama DeleteAsync. Poiché le richieste HTTP DELETE in genere non contengono alcun corpo, il metodo DeleteAsync non fornisce un overload che accetta un'istanza di HttpContent.

Per saperne di più sull'uso di diversi verbi HTTP con HttpClient, vedi HttpClient.

Gestione della durata di HttpClient

Viene restituita una nuova istanza di HttpClient per ogni chiamata di CreateClient per IHttpClientFactory. Viene creata un'istanza HttpClientHandler per nome client. La factory gestisce le durate delle istanze di HttpClientHandler.

IHttpClientFactory memorizza nella cache le istanze di HttpClientHandler create dalla factory per ridurre il consumo di risorse. Un'istanza di HttpClientHandler può essere riusata dalla cache quando si crea una nuova istanza di HttpClient se la relativa durata non è scaduta.

La memorizzazione nella cache dei gestori è consigliabile in quanto ogni gestore gestisce in genere il proprio pool di connessioni HTTP sottostanti. La creazione di più gestori del necessario può comportare l'esaurimento del socket e ritardi di connessione. Alcuni gestori mantengono inoltre le connessioni aperte a tempo indefinito. Ciò può impedire al gestore di reagire alle modifiche DNS.

La durata del gestore predefinito è di due minuti. Per eseguire l'override del valore predefinito, chiama SetHandlerLifetime per ogni client in IServiceCollection:

services.AddHttpClient("Named.Client")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Importante

Le istanze HttpClient create da IHttpClientFactory sono destinate a breve durata.

  • Il riciclo e la ricreazione di HttpMessageHandler quando la loro durata scade è essenziale per IHttpClientFactory, per garantire che i gestori reagiscano alle modifiche DNS. HttpClient è associato a un'istanza specifica del gestore al momento della creazione, pertanto le nuove istanze HttpClient devono essere richieste in modo tempestivo per garantire che il client ottenga il gestore aggiornato.

  • L'eliminazione di tali istanze HttpClientcreate dalla factory non comporterà l'esaurimento del socket, perché lo smaltimento non attiverà l'eliminazione di HttpMessageHandler. IHttpClientFactory tiene traccia ed elimina le risorse usate per creare istanze HttpClient, in particolare le istanze HttpMessageHandler, non appena scade la durata e HttpClient non le usa più.

Mantenere attiva una singola istanza HttpClient per una durata prolungata è un modello comune che può essere usato come alternativa a IHttpClientFactory, tuttavia, questo modello richiede un'installazione aggiuntiva, ad esempio PooledConnectionLifetime. Puoi usare client di lunga durata con PooledConnectionLifetime, o client di breve durata creati da IHttpClientFactory. Per informazioni sulla strategia da usare nell'app, vedi Linee guida per l'uso dei client HTTP.

Configurare il HttpMessageHandler

Può essere necessario controllare la configurazione dell'elemento HttpMessageHandler interno usato da un client.

Quando si aggiungono client denominati o tipizzati viene restituito un elemento IHttpClientBuilder. È possibile usare il metodo di estensione ConfigurePrimaryHttpMessageHandler per definire un delegato sul IServiceCollection. Il delegato viene usato per creare e configurare l'elemento HttpMessageHandler primario usato dal client:

.ConfigurePrimaryHttpMessageHandler(() =>
{
    return new HttpClientHandler
    {
        AllowAutoRedirect = false,
        UseDefaultCredentials = true
    };
});

La configurazione di HttClientHandler consente di specificare un proxy per l'istanza HttpClient tra le altre varie proprietà del gestore. Per maggiori informazioni, vedi Proxy per client.

Configurazione aggiuntiva

Sono disponibili diverse opzioni di configurazione aggiuntive per il controllo di IHttpClientHandler:

metodo Descrizione
AddHttpMessageHandler Aggiunge un gestore di messaggi aggiuntivo per un oggetto denominato HttpClient.
AddTypedClient Configura un'associazione tra TClient e HttpClient denominato associato a IHttpClientBuilder.
ConfigureHttpClient Aggiunge un delegato che verrà usato per configurare un oggetto HttpClient denominato.
ConfigureHttpMessageHandlerBuilder Aggiunge un delegato che verrà usato per configurare gestori di messaggi tramite HttpMessageHandlerBuilder per un oggetto HttpClient denominato.
ConfigurePrimaryHttpMessageHandler Configura l'oggetto HttpMessageHandler primario dal contenitore di inserimento delle dipendenze per un oggetto HttpClient denominato.
RedactLoggedHeaders Imposta la raccolta di nomi di intestazioni HTTP per cui è necessario che i valori vengano oscurati prima della registrazione.
SetHandlerLifetime Imposta l'intervallo di tempo per cui un'istanza di HttpMessageHandler può essere riutilizzata. Per ogni client denominato può essere configurato un valore di durata del gestore.

Uso di IHttpClientFactory insieme a SocketsHttpHandler

L'implementazione SocketsHttpHandler di HttpMessageHandler è stata aggiunta in .NET Core 2.1, il che consente di configurare PooledConnectionLifetime. Questa impostazione viene usata per garantire che il gestore reagisca alle modifiche DNS, pertanto l'uso di SocketsHttpHandler viene considerato un'alternativa all'uso di IHttpClientFactory. Per altre informazioni, vedi Linee guida per l'uso dei client HTTP.

Tuttavia, SocketsHttpHandler e IHttpClientFactory possono essere usati insieme per migliorare la configurabilità. Usando entrambe queste API, è possibile sfruttare la configurabilità sia a un livello basso (ad esempio, usando LocalCertificateSelectionCallback per la selezione dinamica dei certificati) sia a un livello elevato (ad esempio, sfruttando l'integrazione di inserimento delle dipendenze e diverse configurazioni client).

Per usare entrambe le API:

  1. Specificare SocketsHttpHandler come PrimaryHandler e impostare PooledConnectionLifetime (ad esempio, su un valore precedentemente in HandlerLifetime).
  2. Poiché SocketsHttpHandler gestirà il pool di connessioni e il riciclo, il riciclo del gestore a livello di IHttpClientFactory non è più necessario. Puoi disabilitarlo impostando HandlerLifetime su Timeout.InfiniteTimeSpan.
services.AddHttpClient(name)
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new SocketsHttpHandler()
        {
            PooledConnectionLifetime = TimeSpan.FromMinutes(2)
        };
    })
    .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // Disable rotation, as it is handled by PooledConnectionLifetime

Evitare client tipizzati nei servizi singleton

Quando si usa l'approccio client denominato, IHttpClientFactory viene inserito nei servizi e le istanze HttpClient vengono create chiamando CreateClient ogni volta che HttpClient è necessario.

Tuttavia, con l'approccio client tipizzato, i client tipizzati sono oggetti temporanei in genere inseriti nei servizi. Ciò può causare un problema perché un client tipizzato può essere inserito in un servizio singleton.

Importante

I client tipizzati devono essere di breve durata nello stesso senso delle istanze HttpClient create da IHttpClientFactory (per altre informazioni, vedere Gestione della durata HttpClient). Non appena viene creata un'istanza client tipizzata, IHttpClientFactory non ha alcun controllo su di essa. Se un'istanza client tipizzata viene acquisita in un singleton, può impedire che reagisca alle modifiche DNS, vanificando uno degli scopi di IHttpClientFactory.

Se devi usare istanze HttpClient in un servizio singleton, prendi in considerazione le opzioni seguenti:

  • Usa invece l'approccio client denominato, inserendo IHttpClientFactory nel servizio singleton e ricreando le istanze HttpClient quando necessario.
  • Se è necessario l'approccio client tipizzato, usa SocketsHttpHandler con PooledConnectionLifetime configurato come gestore primario. Per altre informazioni sull'uso di SocketsHttpHandler con IHttpClientFactory, vedi la sezione Uso di IHttpClientFactory insieme a SocketsHttpHandler.

Ambiti del gestore di messaggi in IHttpClientFactory

IHttpClientFactory crea un ambito di inserimento delle dipendenze separato per ogni istanza HttpMessageHandler. Questi ambiti di inserimento delle dipendenze sono separati dagli ambiti di inserimento delle dipendenze dell'applicazione, ad esempio l’ambito delle richieste in ingresso ASP.NET, l’ambito di inserimento delle dipendenze manuale creato dall'utente, perciò non condivideranno le istanze di servizio di ambito. Gli ambiti del gestore di messaggi sono associati alla durata del gestore e possono avere un livello di durata maggiore degli ambiti dell'applicazione, che possono causare, ad esempio, il riutilizzo della stessa istanza HttpMessageHandler con le stesse dipendenze con ambito inserito tra diverse richieste in ingresso.

Diagramma che mostra due ambiti di inserimento delle dipendenze dell'applicazione e un ambito separato del gestore di messaggi

È consigliabile non memorizzare nella cache le informazioni correlate all'ambito (ad esempio i dati di HttpContext) all'interno delle istanze HttpMessageHandler e usare le dipendenze di ambito con cautela per evitare la perdita di informazioni riservate.

Se hai bisogno di accedere a un ambito di inserimento delle dipendenze dell'app dal gestore dei messaggi, ad esempio per l'autenticazione incapsulerai la logica con riconoscimento dell'ambito in un DelegatingHandler temporaneo separato ed eseguirai il wrapping in un'istanza HttpMessageHandler dalla cache IHttpClientFactory. Per accedere alla chiamata IHttpMessageHandlerFactory.CreateHandler del gestore per qualsiasi client denominato registrato. In tal caso, creerai un'istanza HttpClient usando il gestore costruito.

Diagramma che mostra l'accesso agli ambiti di inserimento delle dipendenze dell'app tramite un gestore di messaggi temporanei separato e IHttpMessageHandlerFactory

Nell'esempio seguente viene illustrata la creazione di HttpClient con DelegatingHandler con riconoscimento dell'ambito:

if (scopeAwareHandlerType != null)
{
    if (!typeof(DelegatingHandler).IsAssignableFrom(scopeAwareHandlerType))
    {
        throw new ArgumentException($"""
            Scope aware HttpHandler {scopeAwareHandlerType.Name} should
            be assignable to DelegatingHandler
            """);
    }

    // Create top-most delegating handler with scoped dependencies
    scopeAwareHandler = (DelegatingHandler)_scopeServiceProvider.GetRequiredService(scopeAwareHandlerType); // should be transient
    if (scopeAwareHandler.InnerHandler != null)
    {
        throw new ArgumentException($"""
            Inner handler of a delegating handler {scopeAwareHandlerType.Name} should be null.
            Scope aware HttpHandler should be registered as Transient.
            """);
    }
}

// Get or create HttpMessageHandler from HttpClientFactory
HttpMessageHandler handler = _httpMessageHandlerFactory.CreateHandler(name);

if (scopeAwareHandler != null)
{
    scopeAwareHandler.InnerHandler = handler;
    handler = scopeAwareHandler;
}

HttpClient client = new(handler);

Un'ulteriore soluzione alternativa può essere seguita da un metodo di estensione per la registrazione di un DelegatingHandler con riconoscimento dell'ambito e l'override di una registrazione predefinita IHttpClientFactory da parte di un servizio temporaneo con accesso all'ambito dell'app corrente:

public static IHttpClientBuilder AddScopeAwareHttpHandler<THandler>(
    this IHttpClientBuilder builder) where THandler : DelegatingHandler
{
    builder.Services.TryAddTransient<THandler>();
    if (!builder.Services.Any(sd => sd.ImplementationType == typeof(ScopeAwareHttpClientFactory)))
    {
        // Override default IHttpClientFactory registration
        builder.Services.AddTransient<IHttpClientFactory, ScopeAwareHttpClientFactory>();
    }

    builder.Services.Configure<ScopeAwareHttpClientFactoryOptions>(
        builder.Name, options => options.HttpHandlerType = typeof(THandler));

    return builder;
}

Per altre informazioni, vedi l'esempio completo.

Vedi anche