Condividi tramite


Supporto per l'inserimento delle dipendenze con chiave in IHttpClientFactory

Questo articolo illustra come integrare IHttpClientFactory con i servizi chiave.

Keyed Services (detto anche Keyed DI) è una funzionalità di inserimento delle dipendenze (DI) che consente di operare facilmente con più implementazioni di un singolo servizio. Dopo la registrazione, è possibile associare chiavi del servizio diverse alle implementazioni specifiche. In fase di esecuzione, questa chiave viene usata nella ricerca in combinazione con un tipo di servizio, il che significa che è possibile recuperare un'implementazione specifica passando la chiave corrispondente. Per ulteriori informazioni sui Servizi basati su chiave e sull'inserimento delle dipendenze in generale, vedere inserimento delle dipendenze .NET.

Per una panoramica su come usare IHttpClientFactory nell'applicazione .NET, vedere IHttpClientFactory con .NET.

Sfondo

IHttpClientFactory e istanze denominate HttpClient, come prevedibile, si allineano bene all'idea dei Servizi associati. Storicamente, tra le altre cose, IHttpClientFactory era un modo per superare questa funzionalità di Dependency Injection da lungo tempo assente. Tuttavia, i client Named semplici richiedono di ottenere, archiviare ed eseguire query sull'istanza di IHttpClientFactory, invece di iniettare un HttpClientconfigurato, il che potrebbe essere scomodo. Sebbene i client tipiti tentino di semplificare tale parte, viene fornito con un catch: i client tipizzato sono facili da configurazione errata e uso improprioe l'infrastruttura di supporto può anche essere un sovraccarico tangibile in determinati scenari (ad esempio, su piattaforme mobili).

A partire da .NET 9 (versione pacchettiMicrosoft.Extensions.Http e Microsoft.Extensions.DependencyInjection9.0.0+), IHttpClientFactory può sfruttare direttamente il Keyed DI, introducendo un nuovo "Approccio Keyed DI" (anziché gli approcci "denominati" e "tipizzati"). Approccio DI con chiave associa le registrazioni HttpClient utili e altamente configurabili con l'iniezione diretta delle istanze specifiche configurate di HttpClient.

Utilizzo di base

A partire da .NET 9, è necessario acconsentire alla funzionalità chiamando il metodo di estensione AddAsKeyed. Se si aderisce, il client denominato che applica la configurazione viene aggiunto al contenitore DI come servizio di HttpClient con chiave, utilizzando il nome del client come chiave del servizio, in modo da poter utilizzare le API standard dei servizi con chiave (ad esempio, FromKeyedServicesAttribute) per ottenere le istanze denominate HttpClient desiderate (create e configurate da IHttpClientFactory). Per impostazione predefinita, i client vengono registrati con durata con ambito.

Il codice seguente illustra l'integrazione tra IHttpClientFactory, l'inserimento delle dipendenze con chiave e ASP.NET API minime core 9.0:

var builder = WebApplication.CreateBuilder(args);

// --- (1) Registration ---
builder.Services.AddHttpClient("github", c =>
    {
        c.BaseAddress = new Uri("https://api.github.com/");
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
        c.DefaultRequestHeaders.Add("User-Agent", "dotnet");
    })
    .AddAsKeyed(); // Add HttpClient as a Keyed Scoped service for key="github"

var app = builder.Build();

// --- (2) Obtaining HttpClient instance ---
// Directly inject the Keyed HttpClient by its name
app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) =>
    // --- (3) Using HttpClient instance ---
    httpClient.GetFromJsonAsync<Repo>("/repos/dotnet/runtime"));

app.Run();

record Repo(string Name, string Url);

Risposta dell'endpoint:

> ~  curl http://localhost:5000/
{"name":"runtime","url":"https://api.github.com/repos/dotnet/runtime"}

Nell'esempio, il HttpClient configurato viene iniettato nel gestore delle richieste tramite l'infrastruttura DI con chiave standard, integrata nell'associazione dei parametri di ASP.NET Core. Per ulteriori informazioni sui servizi chiave in ASP.NET Core, consulta l'inserimento delle dipendenze in ASP.NET Core.

Confronto tra approcci con chiave, denominati e tipizzati

Si consideri solo il codice correlato al IHttpClientFactorydall'esempio di utilizzo di base :

services.AddHttpClient("github", /* ... */).AddAsKeyed();                // (1)

app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) => // (2)
    //httpClient.Get....                                                 // (3)

Questo frammento di codice illustra come possono apparire la registrazione (1), l'ottenimento dell'istanza configurata HttpClient(2), e l'utilizzo dell'istanza client ottenuta secondo necessità (3) quando si utilizza l'approccio di inserimento delle dipendenze tramite chiave .

Confrontare il modo in cui vengono raggiunti gli stessi passaggi con i due approcci "meno recenti".

Prima di tutto, con l'approccio denominato :

services.AddHttpClient("github", /* ... */);                          // (1)

app.MapGet("/github", (IHttpClientFactory httpClientFactory) =>
{
    HttpClient httpClient = httpClientFactory.CreateClient("github"); // (2)
    //return httpClient.Get....                                       // (3)
});

In secondo luogo, con l'approccio tipizzato :

services.AddHttpClient<GitHubClient>(/* ... */);          // (1)

app.MapGet("/github", (GitHubClient gitHubClient) =>
    gitHubClient.GetRepoAsync());

public class GitHubClient(HttpClient httpClient)          // (2)
{
    private readonly HttpClient _httpClient = httpClient;

    public Task<Repo> GetRepoAsync() =>
        //_httpClient.Get....                             // (3)
}

Tra le tre, il metodo con inserimento di chiavi offre il modo più sintetico per ottenere lo stesso comportamento.

Convalida predefinita del contenitore di inserimento delle dipendenze

Se è stata abilitata la registrazione con chiave per un client denominato specifico, è possibile accedervi con qualsiasi API di inserimento delle chiavi esistente. Tuttavia, se si tenta erroneamente di usare un nome non ancora abilitato, si ottiene l'eccezione di inserimento delle dipendenze con chiave standard:

services.AddHttpClient("keyed").AddAsKeyed();
services.AddHttpClient("not-keyed");

provider.GetRequiredKeyedService<HttpClient>("keyed"); // OK

// Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("not-keyed");

Inoltre, la durata delimitata dei client può aiutare a intercettare i casi di dipendenze intrappolate.

services.AddHttpClient("scoped").AddAsKeyed();
services.AddSingleton<CapturingSingleton>();

// Throws: Cannot resolve scoped service 'System.Net.Http.HttpClient' from root provider.
rootProvider.GetRequiredKeyedService<HttpClient>("scoped");

using var scope = provider.CreateScope();
scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("scoped"); // OK

// Throws: Cannot consume scoped service 'System.Net.Http.HttpClient' from singleton 'CapturingSingleton'.
public class CapturingSingleton([FromKeyedServices("scoped")] HttpClient httpClient)
//{ ...

Selezione della durata del servizio

Per impostazione predefinita, AddAsKeyed() registra HttpClient come servizio con ambito chiave. È anche possibile specificare in modo esplicito la durata passando il parametro ServiceLifetime al metodo AddAsKeyed():

services.AddHttpClient("explicit-scoped")
    .AddAsKeyed(ServiceLifetime.Scoped);

services.AddHttpClient("singleton")
    .AddAsKeyed(ServiceLifetime.Singleton);

Se si chiama AddAsKeyed() all'interno di una registrazione client tipizzata, solo il client denominato sottostante viene registrato come Keyed. Il client tipizzato stesso continua a essere registrato come servizio transitorio semplice.

Evitare perdite di memoria HttpClient temporanee

Importante

HttpClient è IDisposable, pertanto è consigliabile evitare durata temporanea per le istanze di HttpClient con chiave.

La registrazione del client come servizio transitorio con chiave comporta l'HttpClient e le istanze di HttpMessageHandleracquisite dal contenitore DI, poiché entrambi implementano IDisposable. Ciò può comportare perdite di memoria se il client viene risolto più volte all'interno dei servizi Singleton.

Evitare dipendenze in cattività

Importante

Se HttpClient è registrato, scegli una delle seguenti opzioni:

  • come singleton con chiave, -OR-
  • come chiave con ambito o transitorio, e inseriti in un ambito dell'applicazione a esecuzione prolungata (più lunga di HandlerLifetime), -OR-
  • come temporaneo con chiavee inserito in un servizio singleton di,

: l'istanza di HttpClient diventa captivee probabilmente avrà una durata HandlerLifetimeprevista. IHttpClientFactory non ha alcun controllo sui client captive, non sono in grado di partecipare alla rotazione dei gestori e questo può comportare la perdita delle modifiche DNS. Un problema simile esiste già per i clienti tipizzati, registrati come servizi transitori.

Nei casi in cui la longevità del cliente non può essere evitata, o se è coscientemente desiderata, ad esempio per un Singleton con chiave, è consigliabile sfruttare SocketsHttpHandler impostando PooledConnectionLifetime su un valore ragionevole.

services.AddHttpClient("shared")
    .AddAsKeyed(ServiceLifetime.Singleton) // explicit singleton
    .UseSocketsHttpHandler((h, _) => h.PooledConnectionLifetime = TimeSpan.FromMinutes(2))
    .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // disable rotation
services.AddSingleton<MySingleton>();

public class MySingleton([FromKeyedServices("shared")] HttpClient shared) // { ...

Attenzione alla differenza di ambito

Anche se la durata di ambito è molto meno problematica per gli HttpClientdenominati (rispetto alle insidie dei Singleton e dei Transient), ha un proprio tranello.

Importante

La durata con ambito chiave di un'istanza di HttpClient specifica è associata, come previsto, all'ambito dell'applicazione "ordinaria", ad esempio l'ambito della richiesta in ingresso, dalla quale è stata risolta. Tuttavia, non si applica alla catena gestore di messaggi sottostante, che è ancora gestita dal IHttpClientFactory, nello stesso modo in cui è per i clienti Denominati creati direttamente dalla factory. HttpClient con lo stesso, ma risolti (in un intervallo di tempo HandlerLifetime) in due ambiti diversi (ad esempio, due richieste simultanee allo stesso endpoint), possono riutilizzare la stessa HttpMessageHandler istanza di. Tale istanza, a sua volta, ha un proprio ambito separato, come illustrato negli ambiti del gestore del messaggio .

Nota

Il problema di mancata corrispondenza dell'ambito è brutto e esistente e, a partire da .NET 9, rimane ancora non risolto. Da un servizio inserito tramite l'infrastruttura DI regolare, ci si aspetterebbe che tutte le dipendenze fossero soddisfatte dallo stesso ambito; tuttavia, per le istanze con ambito chiave HttpClient, purtroppo non è così.

Catena di gestori messaggi con chiave

Per alcuni scenari avanzati, è possibile accedere direttamente alla catena di HttpMessageHandler anziché a un oggetto HttpClient. IHttpClientFactory fornisce IHttpMessageHandlerFactory'interfaccia per creare i gestori; e se si abilita l'inserimento delle dipendenze con chiave, non solo HttpClient, ma anche la rispettiva catena di HttpMessageHandler viene registrata come servizio con chiave:

services.AddHttpClient("keyed-handler").AddAsKeyed();

var handler = provider.GetRequiredKeyedService<HttpMessageHandler>("keyed-handler");
var invoker = new HttpMessageInvoker(handler, disposeHandler: false);

Procedura: Passare dall'approccio tipizzato all'inserimento delle dipendenze con chiave

Nota

Attualmente, consigliamo di utilizzare l'approccio Keyed DI anziché i clienti di tipo.

Un cambiamento minimo da un client tipizzato (Typed) esistente a una dipendenza basata su chiave può essere simile al seguente:

- services.AddHttpClient<Service>(         // (1) Typed client
+ services.AddHttpClient(nameof(Service),  // (1) Named client
      c => { /* ... */ }                   // HttpClient configuration
  //).Configure....
- );
+ ).AddAsKeyed();                          // (1) + Keyed DI opt-in

+ services.AddTransient<Service>();        // (1) Plain Transient service

  public class Service(
-                                          // (2) "Hidden" Named dependency
+     [FromKeyedServices(nameof(Service))] // (2) Explicit Keyed dependency
      HttpClient httpClient) // { ...

Nell'esempio:

  1. La registrazione del client tipizzato Service è suddivisa in:
    • Registrazione di un client denominato nameof(Service) con la stessa configurazione HttpClient e un consenso esplicito all'inserimento delle dipendenze con chiave; e
    • Servizio temporaneo semplice Service.
  2. HttpClient dipendenza in Service è collegata in modo esplicito a un servizio con chiave specifica nameof(Service).

Il nome non deve essere nameof(Service), ma l'esempio ha lo scopo di ridurre al minimo le modifiche comportamentali. Internamente, i client tipizzati usano client nominati e, per impostazione predefinita, tali client nominati "nascosti" usano il nome del tipo del client tipizzato collegato. In questo caso, il nome "nascosto" è stato nameof(Service), quindi l'esempio lo ha mantenuto.

Tecnicamente, l'esempio "scopre" il client tipizzato, in modo che il client nominato "nascosto" in precedenza venga "esposto" e la dipendenza venga soddisfatta tramite l'infrastruttura DI con chiave anziché quella del client tipizzato.

Procedura: Acconsentire esplicitamente all'inserimento delle dipendenze con chiave per impostazione predefinita

Non è necessario chiamare AddAsKeyed per ogni singolo client. È possibile scegliere facilmente l'opzione "globale" (per qualsiasi nome client) tramite ConfigureHttpClientDefaults. Dal punto di vista dei servizi chiave, si ottiene la registrazione KeyedService.AnyKey.

services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());

services.AddHttpClient("first", /* ... */);
services.AddHttpClient("second", /* ... */);
services.AddHttpClient("third", /* ... */);

public class MyController(
    [FromKeyedServices("first")] HttpClient first,
    [FromKeyedServices("second")] HttpClient second,
    [FromKeyedServices("third")] HttpClient third)
//{ ...

Attenzione ai client "sconosciuti"

Nota

KeyedService.AnyKey Le registrazioni definiscono un mapping da qualunque valore chiave a un'istanza del servizio. Di conseguenza, la convalida del contenitore non viene applicata e un valore di chiave errato comporta l'inserimento di un'istanza errata.

Importante

Per Keyed HttpClients, un errore nel nome del client può causare erroneamente l'inserimento di un client "sconosciuto", ovvero un client il cui nome non è mai stato registrato.

Lo stesso vale per i clienti denominati semplici: IHttpClientFactory non richiede che il nome del cliente venga registrato in modo esplicito (allineandosi al funzionamento del modello Opzioni ). La fabbrica fornisce unHttpClient non configurato, o, più precisamente, configurato per impostazione predefinita, per qualsiasi nome sconosciuto.

Nota

È quindi importante tenere presente che l'approccio "Keyed by default" riguarda non solo tutti i registratiHttpClient, ma tutti i client che IHttpClientFactory è in grado di creare.

services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());
services.AddHttpClient("known", /* ... */);

provider.GetRequiredKeyedService<HttpClient>("known");   // OK
provider.GetRequiredKeyedService<HttpClient>("unknown"); // OK (unconfigured instance)

Considerazioni sulla strategia di "consenso esplicito"

Anche se il consenso esplicito "globale" è un one-liner, è sfortunato che la funzionalità lo richieda ancora, invece di semplicemente lavorare "fuori dalla scatola". Per il contesto completo e il ragionamento su tale decisione, vedere dotnet/runtime#89755 e dotnet/runtime#104943. In breve, il principale ostacolo per "attivato per impostazione predefinita" è la questione del ServiceLifetime: per lo stato attuale (9.0.0) della Iniezione di Dipendenze e delle implementazioni IHttpClientFactory, non esiste un unico ServiceLifetime che sia ragionevolmente sicuro per tutti i HttpClientin tutte le situazioni possibili. Tuttavia, c'è un'intenzione di affrontare le avvertenze nelle prossime versioni e cambiare la strategia da "consenso esplicito" a "rifiutare esplicitamente".

Procedura: Rifiutare esplicitamente la registrazione con chiave

È possibile rifiutare esplicitamente l'inserimento delle dipendenze a chiave per HttpClients chiamando il metodo di estensione RemoveAsKeyed, per nome del client:

services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());      // opt IN by default
services.AddHttpClient("keyed", /* ... */);
services.AddHttpClient("not-keyed", /* ... */).RemoveAsKeyed(); // opt OUT per name

provider.GetRequiredKeyedService<HttpClient>("keyed");     // OK
provider.GetRequiredKeyedService<HttpClient>("not-keyed"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("unknown");   // OK (unconfigured instance)

Oppure "in modo globale" con ConfigureHttpClientDefaults:

services.ConfigureHttpClientDefaults(b => b.RemoveAsKeyed()); // opt OUT by default
services.AddHttpClient("keyed", /* ... */).AddAsKeyed();      // opt IN per name
services.AddHttpClient("not-keyed", /* ... */);

provider.GetRequiredKeyedService<HttpClient>("keyed");     // OK
provider.GetRequiredKeyedService<HttpClient>("not-keyed"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("unknown");   // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.

Ordine di precedenza

Se chiamati insieme o uno qualsiasi di essi più di una volta, AddAsKeyed() e RemoveAsKeyed() in genere seguono le regole delle configurazioni di IHttpClientFactory e delle registrazioni per l'inserimento delle dipendenze (DI):

  1. Se viene chiamato per lo stesso nome, l'ultima impostazione vince: la durata dell'ultimo AddAsKeyed() viene usata per creare la registrazione con chiave (a meno che non sia stato chiamato RemoveAsKeyed() ultimo, nel qual caso il nome viene escluso).
  2. Se usato solo all'interno di ConfigureHttpClientDefaults, l'ultima impostazione prevale.
  3. Se sono stati usati sia ConfigureHttpClientDefaults che un nome client specifico, tutte le impostazioni predefinite vengono considerate avvenute prima di tutte le impostazioni per nome specifico. Pertanto, le impostazioni predefinite possono essere ignorate e l'ultima delle impostazioni per nome prevale.

Vedere anche