Condividi tramite


IHttpClientFactory con .NET

In questo articolo si apprenderà come usare l'interfaccia IHttpClientFactory per creare tipi HttpClient con diversi concetti fondamentali di .NET, ad esempio inserimento delle dipendenze, registrazione e configurazione. Il tipo HttpClient è stato introdotto in .NET Framework 4.5, rilasciato nel 2012. In altre parole, è in circolazione da 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 di fabbrica in grado di 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

Il codice sorgente di esempio fornito in questo articolo richiede l'installazione del pacchetto NuGet Microsoft.Extensions.Http. Inoltre, gli esempi di codice illustrano l'utilizzo delle richieste HTTP GET per recuperare gli oggetti utente Todo dall'API segnaposto {JSON} gratuita.

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 logiche di HttpClient.
  • 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 i problemi comuni del Sistema dei Nomi a Dominio (Domain Name System) che si verificano quando si gestisce manualmente la durata di 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.

Clienti nominati

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 sotto "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.

Clienti tipizzati

Client tipizzati:

  • Offrono le stesse funzionalità dei client denominati senza la necessità di usare le stringhe come chiavi.
  • Fornire l'aiuto di IntelliSense e del compilatore durante l'utilizzo dei 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.
  • Lavora con l'inserimento delle dipendenze (DI) e può essere iniettato dove richiesto 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. Crea 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 ulteriori informazioni, consultare 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 HttpClient come parametro. Inoltre, il tipo TClient non deve essere registrato separatamente con il contenitore DI, perché in questo modo l'ultima registrazione sovrascriverà la prima.

Clienti 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 utilizzata dove necessario, con l'implementazione fornita da Dependency Injection e 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 del ciclo di vita di HttpClient

Viene restituita una nuova istanza di HttpClient per ogni chiamata di CreateClient per IHttpClientFactory. Viene creata un'istanza HttpClientHandler per ogni nome del cliente. La fabbrica gestisce i cicli di vita 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 sostituire il valore predefinito, chiama SetHandlerLifetime per ogni client su 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.
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 i valori devono essere oscurati prima della registrazione.
SetHandlerLifetime Imposta l'intervallo di tempo per cui un'istanza di HttpMessageHandler può essere riutilizzata. Per ogni client nominato è possibile configurare un valore di durata del gestore configurabile.
UseSocketsHttpHandler Configura un'istanza SocketsHttpHandler nuova o già aggiunta dal contenitore di inversione delle dipendenze da utilizzare come gestore principale per un HttpClient con nome. (Solo .NET 5+)

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 e diverse configurazioni client).

Per usare entrambe le API:

  1. Specificare SocketsHttpHandler come PrimaryHandler tramite ConfigurePrimaryHttpMessageHandler o UseSocketsHttpHandler (solo .NET 5+).
  2. Configurare SocketsHttpHandler.PooledConnectionLifetime in base all'intervallo previsto per l'aggiornamento del DNS; ad esempio a un valore precedentemente incluso in HandlerLifetime.
  3. (Opzionale) Dato che gestirà il pool di connessioni e il riciclo, il riciclo a livello di gestione non è più richiesto. Puoi disabilitarlo impostando HandlerLifetime su Timeout.InfiniteTimeSpan.
services.AddHttpClient(name)
    .UseSocketsHttpHandler((handler, _) =>
        handler.PooledConnectionLifetime = TimeSpan.FromMinutes(2)) // Recreate connection every 2 minutes
    .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // Disable rotation, as it is handled by PooledConnectionLifetime

Nell'esempio precedente sono stati scelti arbitrariamente 2 minuti a scopo illustrativo, allineandosi a un valore di HandlerLifetime predefinito. È preferibile scegliere il valore in base alla frequenza prevista delle modifiche al DNS o di altra rete. Per altre informazioni, vedere la sezione Comportamento del DNS nelle linee guida di HttpClient e la sezione Osservazioni nella documentazione sull'API PooledConnectionLifetime.

Evitare l'uso di client tipizzati nei servizi di tipo 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 denominato client, 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 separato per ciascuna istanza di HttpMessageHandler. Questi ambiti DI sono separati dagli ambiti DI dell'applicazione, come l’ambito delle richieste in ingresso ASP.NET o un ambito DI manuale creato dall'utente, quindi non condivideranno le istanze di servizio ambito. Gli ambiti del gestore di messaggi sono associati al ciclo di vita del gestore e possono superare il ciclo di vita degli ambiti dell'applicazione, il che può provocare, ad esempio, il riutilizzo della stessa istanza HttpMessageHandler con le stesse dipendenze scopoate iniettate tra diverse richieste in ingresso.

Diagramma che mostra due ambiti DI dell'applicazione e un ambito separato per il 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 dell'app dal gestore dei messaggi, ad esempio per l'autenticazione, incapsulerai la logica sensibile all'ambito in un DelegatingHandler temporaneo separato e la avvolgerai attorno a un'istanza HttpMessageHandler dalla cache IHttpClientFactory. Per accedere alla chiamata IHttpMessageHandlerFactory.CreateHandler del gestore per qualsiasi client denominato registrato. In tal caso, creeresti 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 mostrata la creazione di un HttpClient con un 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);

Si può adottare un'ulteriore soluzione alternativa con un metodo di estensione per la registrazione di un DelegatingHandler consapevole dell'ambito, sostituendo la registrazione predefinita IHttpClientFactory tramite un servizio transitorio che ha accesso all'ambito attuale dell'app.

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.

Evitare di dipendere dal gestore primario "factory-default"

In questa sezione, il termine "gestore primario di fabbrica" si riferisce al gestore primario che l'implementazione predefinita IHttpClientFactory (o più precisamente, l'implementazione predefinita HttpMessageHandlerBuilder) assegna se non configurato in alcun modo.

Nota

Il gestore primario "factory-default" è un dettaglio dell'implementazione e suscettibile di modifiche. ❌ EVITARE di dipendere da un'implementazione specifica usata come "factory-default", ad esempio, HttpClientHandler.

Esistono casi in cui è necessario conoscere il tipo specifico di un gestore primario, specialmente quando si lavora su una libreria di classi. Mantenendo la configurazione dell'utente finale, è possibile aggiornare, ad esempio, le proprietà specifiche di HttpClientHandlercome ClientCertificates, UseCookiese UseProxy. Potrebbe essere allettante eseguire il cast del Gestore Primario in HttpClientHandler, cosa che è riuscita a funzionare mentre HttpClientHandler veniva usato come Gestore Primario con impostazione predefinita di fabbrica. Tuttavia, come qualsiasi codice che dipende dai dettagli di implementazione, tale soluzione alternativa è fragile e destinata a rompersi.

Anziché basarsi sul gestore primario "factory-default", è possibile usare ConfigureHttpClientDefaults per configurare un'istanza del gestore primario predefinita a livello di app:

// Contract with the end-user: Only HttpClientHandler is supported.

// --- "Pre-configure" stage ---
// The default is fixed as HttpClientHandler to avoid depending on the "factory-default"
// Primary Handler.
services.ConfigureHttpClientDefaults(b =>
    b.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false }));

// --- "End-user" stage ---
// IHttpClientBuilder builder = services.AddHttpClient("test", /* ... */);
// ...

// --- "Post-configure" stage ---
// The code can rely on the contract, and cast to HttpClientHandler only.
builder.ConfigurePrimaryHttpMessageHandler((handler, provider) =>
    {
        if (handler is not HttpClientHandler h)
        {
            throw new InvalidOperationException("Only HttpClientHandler is supported");
        }

        h.ClientCertificates.Add(GetClientCert(provider, builder.Name));

        //X509Certificate2 GetClientCert(IServiceProvider p, string name) { ... }
    });

In alternativa, è possibile controllare il tipo di gestore primario e configurare le specifiche come i certificati client solo nei tipi di supporto noti (probabilmente, HttpClientHandler e SocketsHttpHandler):

// --- "End-user" stage ---
// IHttpClientBuilder builder = services.AddHttpClient("test", /* ... */);
// ...

// --- "Post-configure" stage ---
// No contract is in place. Trying to configure main handler types supporting client
// certs, logging and skipping otherwise.
builder.ConfigurePrimaryHttpMessageHandler((handler, provider) =>
    {
        if (handler is HttpClientHandler h)
        {
            h.ClientCertificates.Add(GetClientCert(provider, builder.Name));
        }
        else if (handler is SocketsHttpHandler s)
        {
            s.SslOptions ??= new System.Net.Security.SslClientAuthenticationOptions();
            s.SslOptions.ClientCertificates ??= new X509CertificateCollection();
            s.SslOptions.ClientCertificates!.Add(GetClientCert(provider, builder.Name));
        }
        else
        {
            // Log warning
        }

        //X509Certificate2 GetClientCert(IServiceProvider p, string name) { ... }
    });

Vedi anche