Usare IHttpClientFactory per l'implementazione di richieste HTTP resilienti

Suggerimento

Questo contenuto è un estratto dell'eBook "Microservizi .NET: Architettura per le applicazioni .NET incluse in contenitori", disponibile in .NET Docs o come PDF scaricabile gratuitamente e da poter leggere offline.

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

IHttpClientFactory è un contratto implementato da DefaultHttpClientFactory, una solida factory, disponibile a partire da .NET Core 2.1, per la creazione di istanze HttpClient da usare nelle applicazioni.

Problemi con la classe HttpClient originale disponibile in .NET

L’originale e ben nota classe HttpClient può essere usata facilmente, ma in alcuni casi non viene usata correttamente dagli sviluppatori.

Anche se questa classe implementa IDisposable, dichiararlo e instanziarlo all'interno di un'istruzione using non è preferibile perché quando l'oggetto HttpClient viene eliminato, il socket sottostante non viene rilasciato immediatamente, il che può causare un problema di esaurimento del socket. Per altre informazioni su questo problema, vedere il post di blog You're using HttpClient wrong and it is destabilizing your software (Uso errato di HttpClient e conseguente destabilizzazione del software).

La classe HttpClient è pertanto destinata a essere avviata una sola volta e a essere riusata nell'arco della vita di un'applicazione. La creazione di un'istanza di una classe HttpClient per ogni richiesta esaurisce il numero di socket disponibili con carichi voluminosi. Questo problema genera errori SocketException. I possibili approcci per risolvere il problema si basano sulla creazione dell'oggetto HttpClient come oggetto singleton o statico, come illustrato in questo articolo di Microsoft sull'utilizzo della classe HttpClient. Questa può essere una buona soluzione per le app console di breve durata o simili, che vengono eseguite alcune volte al giorno.

Un altro problema che gli sviluppatori si trovano a dover affrontare si verificano quando si usa un'istanza condivisa di HttpClient in processi a esecuzione prolungata. In una situazione in cui HttpClient viene istanziata come singleton o oggetto statico, non riesce a gestire le modifiche DNS come descritto in questo problema del repository GitHub dotnet/runtime.

Tuttavia, il problema non è in realtà con HttpClient in sé, ma con il costruttore predefinito per HttpClient, perché crea una nuova istanza concreta di HttpMessageHandler, ovvero quella che presenta i problemi di esaurimento dei socket e le modifiche DNS menzionati in precedenza.

Per risolvere i problemi indicati in precedenza e rendere gestibili le istanze HttpClient, .NET Core 2.1 ha introdotto due approcci, uno dei quali è IHttpClientFactory. Si tratta di un'interfaccia usata per configurare e creare istanze HttpClient in un'app tramite inserimento delle dipendenze. Fornisce anche estensioni per il middleware basato su Polly per sfruttare i vantaggi della delega dei gestori in HttpClient.

L'alternativa consiste nell'usare SocketsHttpHandler con PooledConnectionLifetime configurato. Tale approccio viene applicato a istanze static o HttpClient singleton di lunga durata. Per altre informazioni sulle diverse strategie, vedere Linee guida HttpClient per .NET.

Polly è una libreria di gestione degli errori temporanei che consente agli sviluppatori di aggiungere resilienza alle applicazioni, usando alcuni criteri predefiniti in modo fluente e thread-safe.

Vantaggi dell'uso di IHttpClientFactory

L'implementazione corrente di IHttpClientFactory, che implementa anche IHttpMessageHandlerFactory, offre i vantaggi seguenti:

  • Offre una posizione centrale per la denominazione e la configurazione di oggetti HttpClient logici. È ad esempio possibile configurare un client (agente di servizio) preconfigurato per l'accesso a un microservizio specifico.
  • Codificare il concetto di middleware in uscita tramite la delega di gestori in HttpClient e l'implementazione di middleware basato su Polly per sfruttarne i criteri di resilienza.
  • HttpClient include già il concetto di delega di gestori concatenati per le richieste HTTP in uscita. È possibile registrare i client HTTP nella factory ed è possibile usare un gestore Polly per utilizzare i criteri Polly per Retry, CircuitBreakers e così via.
  • Gestire la durata di HttpMessageHandler per evitare i problemi menzionati che possono verificarsi quando si gestiscono le durate di HttpClient personalmente.

Suggerimento

Le istanze HttpClient inserite da DI possono essere eliminate in modo sicuro, perché l'oggetto associato HttpMessageHandler è gestito dalla factory. Le istanze HttpClient inserite sono temporanee dal punto di vista del DI, mentre le istanze HttpMessageHandler possono essere considerate con ambito. Le istanze HttpMessageHandler hanno ambiti DI specifici, separati dagli ambiti dell'applicazione, ad esempio, gli ambiti di richiesta in ingresso di ASP.NET. Per altre informazioni, vedere Uso di HttpClientFactory in .NET.

Nota

L'implementazione di IHttpClientFactory (DefaultHttpClientFactory) è strettamente legata all'implementazione di DI nel pacchetto NuGet Microsoft.Extensions.DependencyInjection. Se è necessario usare HttpClient senza DI o con altre implementazioni di DI, è consigliabile usare un static o un HttpClient singleton o con la configurazione PooledConnectionLifetime. Per altre informazioni, vedere Linee guida httpClient per .NET.

Modi diversi di usare IHttpClientFactory

Esistono molti modi per usare IHttpClientFactory nell'applicazione:

  • Utilizzo di base
  • Usare client denominati
  • Usare client tipizzati
  • Usare client generati

Per motivi di brevità, queste linee guida illustrano il modo più strutturato per usare IHttpClientFactory, che consiste nell'usare client tipizzati (modello di agente di servizio). Tuttavia, tutte le opzioni sono documentate e sono attualmente elencate in questo articolo che illustra l'IHttpClientFactoryutilizzo.

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

Come usare i client tipizzati con IHttpClientFactory

Che cos'è un "client tipizzato"? È solo un HttpClient oggetto preconfigurato per un uso specifico. Questa configurazione può includere valori specifici, ad esempio il server di base, le intestazioni HTTP o i timeout.

Il diagramma seguente visualizza l'uso dei client tipizzati con IHttpClientFactory.

Diagram showing how typed clients are used with IHttpClientFactory.

Figura 8-4. Uso di IHttpClientFactory con classi di client tipizzati.

Nell'immagine precedente, un oggetto ClientService (usato da un controller o codice client) usa un oggetto HttpClient creato dall'oggetto registrato IHttpClientFactory. Questa factory assegna un HttpMessageHandler in un pool all'oggetto HttpClient. HttpClient può essere configurato con i criteri di Polly durante la registrazione diIHttpClientFactory nel contenitore di DI con il metodo di estensione AddHttpClient.

Per configurare la struttura precedente, aggiungere IHttpClientFactory nell'applicazione installando il pacchetto NuGet Microsoft.Extensions.Http che include il metodo di estensione AddHttpClientper IServiceCollection. Questo metodo di estensione registra la classe interna DefaultHttpClientFactory da usare come singleton per l'interfaccia IHttpClientFactory. Definisce una configurazione temporanea per l'oggetto HttpMessageHandlerBuilder. Questo gestore di messaggi (oggetto HttpMessageHandler), estratto da un pool, viene usato dall'oggetto HttpClient restituito dalla factory.

Nel frammento di contenuto successivo è possibile vedere come usare AddHttpClient() per registrare i client tipizzati (agenti di servizio) che devono usare HttpClient.

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

La registrazione dei servizi client, come illustrato nel frammento di codice precedente, rende la creazione di DefaultClientFactory uno standard HttpClient per ogni servizio. Il client tipizzato viene registrato come temporaneo nel contenitore DI. Nel codice precedenteAddHttpClient() registra CatalogService, BasketService, OrderingService come servizi temporanei, in modo che possano essere inseriti e utilizzati direttamente senza la necessità di registrazioni aggiuntive.

È inoltre possibile aggiungere una configurazione specifica dell'istanza nella registrazione, ad esempio configurare l'indirizzo di base e aggiungere alcuni criteri di resilienza, come illustrato di seguito:

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

Nell’esempio seguente è possibile visualizzare la configurazione di uno dei criteri precedenti:

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

Per altre informazioni sull'uso di Polly, vedere l'articolo successivo.

Durate di HttpClient

Ogni volta che si ottiene un oggetto HttpClient da IHttpClientFactory viene restituita una nuova istanza. Tuttavia ogni HttpClient usa un HttpMessageHandler in pool e riusato da IHttpClientFactory per ridurre il consumo di risorse, a condizione che la durata di HttpMessageHandler non sia scaduta.

Il pooling dei gestori è opportuno poiché ogni gestore si occupa in genere di gestire le proprie connessioni HTTP sottostanti. La creazione di un numero superiore di gestori rispetto a quelli necessari può determinare ritardi di connessione. Alcuni gestori mantengono inoltre le connessioni aperte a tempo indefinito. Ciò può impedire al gestore di reagire alle modifiche DNS.

Gli oggetti HttpMessageHandler nel pool hanno una durata, che è l'intervallo di tempo in cui un'istanza di HttpMessageHandler nel pool può essere usata nuovamente. Il valore predefinito è due minuti, ma può essere sostituito in base al cliente tipizzato. Per eseguire l'override, chiamare SetHandlerLifetime() nell'elemento IHttpClientBuilder restituito al momento della creazione del client, come illustrato nel codice seguente:

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

Ogni client tipizzato o client denominato può avere un proprio valore configurato di durata del gestore. Impostare la durata su InfiniteTimeSpan per disabilitare la scadenza del gestore.

Implementare le classi di client tipizzato che usano l'oggetto HttpClient inserito e configurato

È necessario avere definito in precedenza le classi di client tipizzato, ad esempio le classi nell'esempio di codice come "BasketService", "CatalogService", "OrderingService" e così via. Un client tipizzato è una classe che accetta un oggetto HttpClient (inserito tramite il relativo costruttore) e lo usa per chiamare un servizio HTTP remoto. Ad esempio:

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

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

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

        var responseString = await _httpClient.GetStringAsync(uri);

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

Il client tipizzato (CatalogService nell'esempio) viene attivato da DI (inserimento delle dipendenze), ovvero può accettare qualsiasi servizio registrato nel relativo costruttore, oltre a HttpClient.

Un client tipizzato è in effetti un oggetto temporaneo, ovvero viene creata una nuova istanza ogni volta che ne è necessaria una. Riceve una nuova istanza HttpClient ogni volta che viene costruita. Tuttavia, gli oggetti HttpMessageHandler nel pool sono gli oggetti riutilizzati da più istanze HttpClient.

Uso di classi di client tipizzato

Infine, dopo aver implementato le classi tipizzate, è possibile registrarle e configurarle con AddHttpClient(). Dopodiché, è possibile utilizzare ovunque i servizi vengano inseriti tramite DI, ad esempio nel codice della pagina Razor o in un controller di app Web MVC, illustrato nel codice seguente di eShopOnContainers:

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

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

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

        }
}

Fino a questo punto, il frammento di codice precedente mostra solo l'esempio di esecuzione di normali richieste HTTP. Tuttavia, la "magia" è disponibile nelle sezioni seguenti, in cui viene illustrato come tutte le richieste HTTP effettuate da HttpClient possono avere criteri resilienti, ad esempio i tentativi con backoff esponenziale, interruttori di circuito, funzionalità di sicurezza che usano token di autenticazione o anche qualsiasi altra funzionalità personalizzata. E tutte queste operazioni possono essere eseguite aggiungendo criteri e delegando i gestori per i client tipizzati registrati.

Risorse aggiuntive