Teilen über


Unterstützung von Keyed DI in IHttpClientFactory

In diesem Artikel erfahren Sie, wie Sie IHttpClientFactory in Keyed Services integrieren.

Keyed Services (auch als Keyed DIbezeichnet) ist ein Feature zum Einfügen von Abhängigkeiten (Dependency Injection, DI), mit dem Sie bequem mit mehreren Implementierungen eines einzelnen Diensts arbeiten können. Bei der Registrierung können Sie verschiedene Dienstschlüssel den spezifischen Implementierungen zuordnen. Zur Laufzeit wird dieser Schlüssel in einer Suche in Kombination mit einer Dienstart verwendet, was bedeutet, dass Sie eine bestimmte Implementierung abrufen können, indem Sie den übereinstimmenden Schlüssel übergeben. Weitere Informationen zu Keyed Services und DI im Allgemeinen finden Sie unter .NET Dependency Injection.

Eine Übersicht über die Verwendung von IHttpClientFactory in Ihrer .NET-Anwendung finden Sie unter IHttpClientFactory mit .NET.

Hintergrund

IHttpClientFactory und Named HttpClient-Instanzen passen, wenig überraschend, gut zur Idee der Keyed Services. In der Vergangenheit war unter anderem IHttpClientFactory eine Möglichkeit, diese lange vermisste Funktion von DI zu realisieren. Aber bei reinen Named Clients müssen Sie die IHttpClientFactory-Instanz abrufen, speichern und abfragen, anstatt eine konfigurierte HttpClient-Instanz zu injizieren, was unbequem sein kann. Typed Clients versuchen zwar, dieses Element zu vereinfachen, haben aber auch einen Haken: Typed Clients lassen sich leicht fehlkonfigurieren und missbrauchen, und auch die unterstützende Infrastruktur kann in bestimmten Szenarien (z.B. auf mobilen Plattformen) einen spürbaren Mehraufwand darstellen.

Ab .NET 9 (Microsoft.Extensions.Http- und Microsoft.Extensions.DependencyInjection Paketversion 9.0.0+) kann IHttpClientFactory Keyed DI direkt nutzen und einen neuen "Keyed DI-Ansatz" (im Gegensatz zu "Named"- und "Typed"-Ansätzen) einführen. "Der Keyed-DI-Ansatz kombiniert die bequemen, hoch konfigurierbaren HttpClient-Registrierungen mit der einfachen Injektion der spezifisch konfigurierten HttpClient-Instanzen."

Grundlegende Verwendung

Ab .NET 9 müssen Sie für die Funktion ein opt in durchführen, indem Sie die AddAsKeyed-Erweiterungsmethode aufrufen. Wenn sie sich angemeldet haben, wird der benannte Client, der die Konfiguration anwendet, dem DI-Container als Keyed-HttpClient-Dienst hinzugefügt, wobei der Name des Clients als Dienstschlüssel verwendet wird, sodass Sie die standardmäßigen Keyed Services-APIs (z. B. FromKeyedServicesAttribute) verwenden können, um die gewünschten benannten HttpClient Instanzen zu erhalten (erstellt und konfiguriert durch IHttpClientFactory). Standardmäßig werden die Clients mit Scoped Lebensdauer registriert.

Der folgende Code veranschaulicht die Integration zwischen IHttpClientFactory, Keyed DI und ASP.NET Core 9.0 Minimal-APIs:

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);

Endpunktantwort:

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

Im Beispiel wird die konfigurierte HttpClient über die standardmäßige keybasierte DI-Infrastruktur in den Anfrage-Handler eingefügt, die in die Parameterbindung von ASP.NET Core integriert ist. Weitere Informationen zu Keyed Services in ASP.NET Core finden Sie unter Dependency Injection in ASP.NET Core.

Vergleich von Keyed-, Named- und Typed-Ansätzen

Betrachten Sie nur den IHttpClientFactorybezogenen Code aus dem Beispiel Basic Usage:

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

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

Dieses Code-Snippet veranschaulicht, wie die Registrierung (1), das Abrufen der konfigurierten HttpClient-Instanz (2) und die Verwendung der abgerufenen Client-Instanz bei Bedarf (3) bei Verwendung des Keyed DI-Ansatzes aussehen kann.

Vergleichen Sie, wie die gleichen Schritte mit den beiden "älteren" Ansätzen erreicht werden.

Erstens, mit dem Named Ansatz:

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

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

Zweitens, mit dem Typed Ansatz:

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)
}

Aus den drei bietet der Keyed DI-Ansatz die prägnantste Möglichkeit, dasselbe Verhalten zu erreichen.

Integrierte DI-Container-Validierung

Wenn Sie die Registrierung mit Schlüsseln für einen bestimmten benannten Client aktiviert haben, können Sie mit vorhandenen KEYed DI-APIs darauf zugreifen. Wenn Sie jedoch fälschlicherweise versuchen, einen Namen zu verwenden, der noch nicht aktiviert ist, erhalten Sie die Standardausnahme für Keyed DI.

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");

Darüber hinaus kann die Scoped-Lebensdauer der Clients helfen, Fälle von Captive Abhängigkeiten abzufangen:

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)
//{ ...

Auswahl der Dienstlebensdauer

Standardmäßig registriert AddAsKeyed() HttpClient als einen Keyed Scoped Dienst. Sie können die Lebensdauer auch explizit angeben, indem Sie den parameter ServiceLifetime an die AddAsKeyed() Methode übergeben:

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

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

Wenn Sie AddAsKeyed() innerhalb einer typisierten Client-Registrierung aufrufen, wird nur der zugrunde liegende benannte Client als Keyed registriert. Der Typd-Client selbst wird weiterhin als einfacher transienter Dienst registriert.

Vermeiden vorübergehender HttpClient-Speicherlecks

Wichtig

HttpClient ist IDisposable, daher empfehlen wir dringend, die vorübergehende Lebensdauer für HttpClient-Instanzen mit Keyed zu vermeiden.

Die Registrierung des Clients als Keyed Transient Dienst führt dazu, dass die HttpClient- und HttpMessageHandler-Instanzen vom DI Container eingefangen werden, da beide IDisposable implementieren. Dies kann zu Speicherlecks führen, wenn der Client mehrfach innerhalb von Singleton-Diensten aufgelöst wird.

Vermeiden von captive Abhängigkeiten

Wichtig

Wenn HttpClient registriert ist entweder:

  • als Keyed Singleton, -ODER-
  • als Keyed Scoped oder Transient, und innerhalb eines lang laufenden (länger als HandlerLifetime) Bereichs der Anwendung injiziert, -OR-
  • als Keyed Transient, und in einen Singleton Dienst injiziert,

- wird die HttpClient-Instanz captive und überlebt wahrscheinlich ihre erwartete HandlerLifetime Lebensdauer. IHttpClientFactory hat keine Kontrolle über Captive Clients, sie können NICHT an der Handler-Rotation teilnehmen und es kann zu dem Verlust von DNS-Änderungen führen. Ein ähnliches Problem besteht bereits für Typed Clients, die als Transient Services registriert sind.

In Fällen, in denen sich die Langlebigkeit von Clients nicht vermeiden lässt – oder wenn sie bewusst erwünscht ist, z.B. bei einem Keyed Singleton – ist es ratsam, die SocketsHttpHandler zu nutzen, indem Sie PooledConnectionLifetime auf einen vernünftigen Wert festlegen.

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) // { ...

Vorsicht vor Fehlanpassungen im Bereich

Obwohl die Scoped Lebensdauer für die Named HttpClients viel weniger problematisch ist (im Vergleich zu den Singleton und Transient Tücken), hat sie ihren eigenen Haken.

Wichtig

Die Keyed Scoped-Lebensdauer einer bestimmten HttpClient-Instanz ist – wie erwartet – an den "normalen" Bereich der Anwendung (z.B. den Bereich der eingehenden Anfrage) gebunden, aus dem sie aufgelöst wurde. Sie gilt jedoch NICHT für die zugrunde liegende Nachrichtenhandlerkette, die weiterhin vom IHttpClientFactoryverwaltet wird, auf die gleiche Weise wie für die benannten Clients, die direkt aus der Factory erstellt werden. HttpClients mit dem gleichen Namen, die aber (innerhalb eines HandlerLifetimeZeitrahmens) in zwei verschiedenen Bereichen aufgelöst werden (z.B. zwei parallele Anfragen an denselben Endpunkt), können die gleicheHttpMessageHandler Instanz wiederverwenden. Diese Instanz hat wiederum ihren eigenen Bereich, wie in den Message Handler Bereichen dargestellt.

Anmerkung

Das Scope Mismatch Problem ist unangenehm und besteht schon seit langem und ist auch in .NET 9 immer noch ungelöst. Von einem Dienst, der über die normale DI-Infrastruktur eingefügt wurde, erwarten Sie, dass alle Abhängigkeiten vom gleichen Bereich erfüllt werden– aber für die Keyed Scoped-HttpClient Instanzen ist das leider nicht der Fall.

Nachrichtenhandlerkette mit Schlüssel

Bei einigen erweiterten Szenarien möchten Sie anstelle eines HttpClient-Objekts direkt auf die HttpMessageHandler-Kette zugreifen. IHttpClientFactory stellt die IHttpMessageHandlerFactory Schnittstelle zum Erstellen der Handler bereit, und wenn Sie Keyed DI aktivieren, wird nicht nur HttpClient, sondern auch die entsprechende HttpMessageHandler-Kette als Keyed-Dienst registriert.

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

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

Anleitung: Wechsel vom typisierten Ansatz zum Keyed-DI

Anmerkung

Derzeit empfehlen wir die Verwendung des Keyed DI-Ansatzes anstelle von typisierten Clients.

Ein minimaler Wechsel von einem vorhandenen typisierten Client zu einer schlüsselgesteuerten Abhängigkeit kann wie folgt aussehen:

- 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) // { ...

Im Beispiel:

  1. Die Registrierung des Typed Client Service ist aufgeteilt in:
    • Eine Registrierung eines Named Client nameof(Service) mit der gleichen HttpClient-Konfiguration und einer Zustimmung zu Keyed DI; und
    • Einfacher Transient Dienst Service.
  2. Die HttpClient-Abhängigkeit in Service ist explizit an einen Keyed Dienst mit Schlüssel nameof(Service) gebunden.

Der Name muss nicht nameof(Service)sein, aber das Beispiel soll die Verhaltensänderungen minimieren. Intern verwenden Typed Clients Named Clients, und standardmäßig werden solche "versteckten" Named Clients mit dem Typnamen des verlinkten Typed Clients bezeichnet. In diesem Fall war der "versteckte" Name nameof(Service), sodass er im Beispiel beibehalten wurde.

Technisch gesehen "packt" das Beispiel den Typed Client aus, sodass der zuvor "versteckte" Named Client "sichtbar" wird und die Abhängigkeit über die Keyed DI-Infra statt über die Typed Client-Infra erfüllt wird.

So aktivieren Sie standardmäßig Keyed DI

Sie müssen nicht AddAsKeyed für jeden einzelnen Client aufrufen – Sie können sich einfach "global" (für jeden Clientnamen) über ConfigureHttpClientDefaultsanmelden. Aus Sicht der Keyed Dienste führt dies zu einer KeyedService.AnyKey Registrierung.

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)
//{ ...

Vorsicht vor 'unbekannten' Clients

Anmerkung

KeyedService.AnyKey Registrierungen definieren eine Zuordnung von einem beliebigen Schlüsselwert zu einer Dienstinstanz. Das Ergebnis ist jedoch, dass die Container-Validierung nicht greift und ein fehlerhafter Schlüsselwert stumm dazu führt, dass eine falsche Instanz injiziert wird.

Wichtig

Bei Keyed HttpClients kann ein Fehler im Clientnamen dazu führen, dass ein "unbekannter" Client fälschlicherweise injiziert wird, d. h. ein Client, dessen Name nie registriert wurde.

Das gleiche gilt für die einfachen benannten Clients: IHttpClientFactory erfordert nicht, dass der Clientname explizit registriert wird (entsprechend der Funktionsweise des Optionsmusters). Die Factory liefert Ihnen für jeden unbekannten Namen einen unkonfigurierten – oder genauer gesagt, einen standardmäßig konfigurierten – HttpClient.

Anmerkung

Daher ist es wichtig, sich vor Augen zu halten: Der Ansatz "Standardmäßig keyed" deckt nicht nur alle registriertenHttpClient ab, sondern auch alle Clients, die von IHttpClientFactory erstellt werden können.

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

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

Überlegungen zur "Opt-In"-Strategie

Auch wenn das "globale" Opt-in nur ein Einzeiler ist, ist es bedauerlich, dass die Funktion es immer noch erfordert, anstatt einfach "out of the box" zu funktionieren. Den vollständigen Kontext und die Gründe für diese Entscheidung finden Sie unter dotnet/runtime#89755 und dotnet/runtime#104943. Kurz gesagt, der Hauptblocker für "standardmäßig aktiviert" ist die ServiceLifetime "Kontroverse": Für den aktuellen (9.0.0) Status der DI- und IHttpClientFactory-Implementierungen gibt es kein einziges ServiceLifetime, das für alle HttpClient in allen möglichen Situationen einigermaßen sicher wäre. Es ist jedoch beabsichtigt, die Vorbehalte in den bevorstehenden Versionen zu beheben und die Strategie von "Opt-In" auf "Opt-Out" zu wechseln.

Anleitung: Abmeldung von der Schlüsselregistrierung

Sie können sich explizit gegen Keyed DI für HttpClient entscheiden, indem Sie die RemoveAsKeyed-Erweiterungsmethode aufrufen, entweder pro Client-Name:

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)

Oder "global betrachtet" mit 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.

Rangordnung

Wenn sie gemeinsam oder einzeln mehrmals aufgerufen werden, befolgen AddAsKeyed() und RemoveAsKeyed() im Allgemeinen die Regeln der IHttpClientFactory-Einstellungen und DI-Registrierungen.

  1. Wenn sie für denselben Namen aufgerufen wird, gewinnt die letzte Einstellung: die Lebensdauer aus dem letzten AddAsKeyed() wird verwendet, um die Keyed-Registrierung zu erstellen (es sei denn, RemoveAsKeyed() wurde zuletzt aufgerufen, in diesem Fall wird der Name ausgeschlossen).
  2. Wenn sie nur innerhalb ConfigureHttpClientDefaultsverwendet wird, gewinnt die letzte Einstellung.
  3. Wenn sowohl ConfigureHttpClientDefaults als auch der spezifische Client-Name verwendet wurden, werden alle Standardeinstellungen vor allen Einstellungen pro Name als "geschehen" betrachtet. Daher können Standardwerte ignoriert werden, und die letzten einstellungen pro Name haben Vorrang.

Siehe auch