Dela via


Nyckelbaserat DI-stöd i IHttpClientFactory

I den här artikeln lär du dig att integrera IHttpClientFactory med Keyed Services.

Keyed Services (kallas även Keyed DI) är en beroendeinmatningsfunktion (DI) som gör att du enkelt kan arbeta med flera implementeringar av en enda tjänst. Vid registreringen kan du associera olika tjänstnycklar med de specifika implementeringarna. Vid körning används den här nyckeln i uppslag i kombination med en tjänsttyp, vilket innebär att du kan hämta en specifik implementering genom att skicka matchande nyckel. Mer information om Keyed Services och DI i allmänhet finns i .NET-beroendeinmatning.

En översikt över hur du använder IHttpClientFactory i .NET-programmet finns i IHttpClientFactory med .NET.

Bakgrund

IHttpClientFactory och namngivna HttpClient instanser, föga förvånande, överensstämmer väl med Keyed Services-idén. Historiskt sett var bland annat IHttpClientFactory ett sätt att övervinna denna länge saknade DI-funktion. Men vanliga namngivna klienter kräver att du hämtar, lagrar och frågar den IHttpClientFactory instansen – i stället för att mata in en konfigurerad HttpClient– vilket kan vara obekvämt. Även om inskrivna klienter försöker förenkla den delen kommer det med en hake: Inskrivna klienter är lätta att felkonfigurera och missbruk, och den stödjande infrastrukturen kan också vara ett konkret omkostnader i vissa scenarier (till exempel på mobila plattformar).

Från och med .NET 9 (Microsoft.Extensions.Http och Microsoft.Extensions.DependencyInjection paketversion 9.0.0+), kan IHttpClientFactory utnyttja Keyed DI direkt, vilket introducerar en ny "Keyed DI-metod" (i motsats till "Namngivna" och "typade" metoder). "Keyed DI-metod kopplar samman den praktiska, mycket konfigurerbara HttpClient-registreringen med det enkla införandet av de specifika konfigurerade HttpClient-instanserna."

Grundläggande användning

Från och med .NET 9 måste du aktivera funktionen genom att kalla på AddAsKeyed-tilläggsmetoden. Om du väljer det läggs den namngivna klienten som tillämpar konfigurationen till i DI-containern som en keyed HttpClient-tjänst, med klientens namn som en tjänstnyckel, så att du kan använda STANDARD KEYed Services-API:er (till exempel FromKeyedServicesAttribute) för att hämta önskade namngivna HttpClient-instanser (skapade och konfigurerade av IHttpClientFactory). Som standardläge registreras klienterna med avgränsad livslängd.

Följande kod illustrerar integreringen mellan IHttpClientFactory, Keyed DI och ASP.NET Core 9.0 Minimala API:er:

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

Slutpunktssvar:

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

I exemplet matas den konfigurerade HttpClient in i begärandehanteraren via standardnyckelad DI-infrastruktur, som är integrerad i ASP.NET Core-parameterbindning. Mer information om Keyed Services i ASP.NET Core finns i Beroendeinmatning i ASP.NET Core.

Jämförelse av nyckelade, namngivna och typade metoder

Tänk bara på den IHttpClientFactory-relaterade koden från Basic Usage exempel:

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

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

Det här kodfragmentet illustrerar hur registreringen (1), hämtar den konfigurerade HttpClient-instansen (2)och använder den erhållna klientinstansen efter behov (3) kan se ut när du använder metoden Keyed DI.

Jämför hur samma steg uppnås med de två "äldre" metoderna.

Först med den namngivna metoden :

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

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

För det andra, med metoden Typed:

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

Av de tre erbjuder keyed DI-metoden det mest kortfattade sättet att uppnå samma beteende.

Inbyggd DI-containerverifiering

Om du har aktiverat nyckelregistreringen för en specifik namngiven klient kan du komma åt den med alla befintliga NYCKELADE DI-API:er. Men om du av misstag försöker använda ett namn som inte är aktiverat än, får du det standardiserade nyckel-DI-undantaget.

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

Dessutom kan klienternas avgränsade livslängd hjälpa till att fånga upp fall av bindande beroenden.

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

Val av tjänstlivslängd

Som standard registrerar AddAsKeyed()HttpClient som en keyed Scoped-tjänst. Du kan också uttryckligen ange livslängden genom att skicka parametern ServiceLifetime till metoden AddAsKeyed():

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

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

Om du anropar AddAsKeyed() i en typad klientregistrering registreras endast den underliggande namngivna klienten som Nyckelad. Typed-klienten i sig fortsätter att vara registrerad som en vanlig transienttjänst.

Undvik tillfällig HttpClient-minnesläcka

Viktig

HttpClient är IDisposable, så vi rekommenderar starkt undvika tillfällig livslängd för Keyed HttpClient - instanser.

Om klienten registreras som en nyckelbaserad transienttjänst leder det till att instanserna HttpClient och HttpMessageHandler blir upptagna av DI-containern, eftersom de båda implementerar IDisposable. Detta kan leda till minnesläckor om klienten löses flera gånger inom Singleton-tjänster.

Undvik beroendeförhållanden

Viktig

Om HttpClient antingen har registrerats:

  • som en nycklad Singleton , -ELLER-
  • som en nycklad eller flyktigoch injiceras inom en långvarig (längre än HandlerLifetime) tillämpningsomfattning, ELLER
  • som en nyckelad tillfälligaoch matas in i en Singleton-tjänst,

— den HttpClient instansen blir fångeoch kommer sannolikt att överleva sin förväntade HandlerLifetime. IHttpClientFactory har ingen kontroll över captive-klienter, de kan INTE delta i rotationen av hanterare, och det kan resultera i förlust av DNS-ändringar. Ett liknande problem finns redan för typade klienter som är registrerade som transienta tjänster.

I de fall då klientens livslängd inte kan undvikas – eller om det är medvetet önskat, till exempel för en keyed Singleton – rekommenderas det att utnyttja SocketsHttpHandler genom att ange PooledConnectionLifetime till ett rimligt värde.

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

Se upp för oförenlighet i omfång

Även om den avgränsade livstiden är mycket mindre problematisk för namngivna HttpClient(jämfört med fallgropar hos Singleton och Transient), har den sina egna utmaningar.

Viktig

Livslängden för nyckelade omfång för en specifik HttpClient instans är som förväntat bunden till det "vanliga" programomfånget (till exempel inkommande begärandeomfång) där den löstes från. Det gäller dock INTE för den underliggande meddelandehanterarkedjan, som fortfarande hanteras av IHttpClientFactory, på samma sätt som för de namngivna klienter som skapats direkt från fabriken. HttpClientmed samma namn, men löst (inom en HandlerLifetime tidsram) i två olika omfång (till exempel två samtidiga begäranden till samma slutpunkt), kan återanvända sammaHttpMessageHandler instans. Den instansen har i sin tur ett separat omfång, vilket visas i meddelandehanterarens omfång.

Anteckning

Det omfångsmatchningsfelet problemet är otäckt och långvarigt, och från och med .NET 9 är fortfarande olöst. Från en tjänst som matas in via den vanliga DI-infrastrukturen förväntar du dig att alla beroenden uppfylls från samma omfång, men för keyed Scoped HttpClient-instanser är det tyvärr inte fallet.

Nyckelhanterarkedja för meddelande

I vissa avancerade scenarier kanske du vill komma åt HttpMessageHandler kedja direkt i stället för ett HttpClient objekt. IHttpClientFactory tillhandahåller IHttpMessageHandlerFactory gränssnitt för att skapa hanterarna. och om du aktiverar Keyed DI registreras inte bara HttpClient, utan även respektive HttpMessageHandler-kedja som en nyckelbaserad tjänst:

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

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

Anvisningar: Växla från typbaserad metod till Nyckelad DI

Anteckning

Vi rekommenderar för närvarande att du använder den nyckelbaserade DI-metoden i stället för typade klienter.

En minimal ändringsväxling från en befintlig typbaserad klient till ett nyckelberoende kan se ut så här:

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

I exemplet:

  1. Registreringen av den typerade klienten Service är uppdelad i:
    • En registrering av en namngiven klient nameof(Service) med samma HttpClient konfiguration och ett val till Keyed DI.
    • Plain Transient-tjänsten Service.
  2. HttpClient beroende i Service är uttryckligen bundet till en nyckelad tjänst med en nyckel nameof(Service).

Namnet behöver inte vara nameof(Service), men exemplet syftar till att minimera beteendeändringarna. Internt använder typade klienter namngivna klienter, och som standard följer sådana "dolda" namngivna klienter det länkade typade klientens typnamn. I det här fallet var namnet "dolt" nameof(Service), så exemplet bevarade det.

Tekniskt sett "avtäcker" exemplet den typade klienten, så att den tidigare "dolda" namngivna klienten blir "exponerad," och beroendet uppfylls via Keyed DI-infrastruktur i stället för den typade klientens infrastruktur.

Gör så här: Anmäl dig till Nyckelad DI som standard

Du behöver inte anropa AddAsKeyed för varje enskild klient – du kan enkelt välja "globalt" (för alla klientnamn) via ConfigureHttpClientDefaults. Från Keyed Services-perspektivet resulterar det i en KeyedService.AnyKey-registrering.

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

Se upp för "okända" klienter

Anteckning

KeyedService.AnyKey registreringar definierar en mappning från vilket som helst nyckelvärde till en viss tjänstinstans. Därför gäller inte containerverifieringen och ett felaktigt nyckelvärde tyst leder till en fel instans matas in.

Viktig

För nycklade HttpClient:or kan ett misstag i klientnamnet leda till att en "okänd" klient införs felaktigt, det vill säga, en klient vars namn aldrig registrerades.

Detsamma gäller för vanliga namngivna klienter: IHttpClientFactory kräver inte att klientnamnet uttryckligen registreras (i linje med det sätt som mönstret Alternativ fungerar). Fabriken ger dig en ej konfigurerad—eller, mer exakt, standardkonfigurerad—HttpClient för ett okänt namn.

Anteckning

Därför är det viktigt att ha i åtanke: metoden "Nyckelad som standard" omfattar inte bara alla registreradeHttpClients, utan alla klienter som IHttpClientFactorykan skapa.

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

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

Överväganden för "opt-in"-strategi

Även om den "globala" opt-in är en one-liner, är det olyckligt att funktionen fortfarande kräver det, istället för att bara fungera "out of the box". Fullständig kontext och motiveringen bakom det beslutet finns i dotnet/runtime#89755 och dotnet/runtime#104943. I korthet är huvudhindret för "på som standard" ServiceLifetime "tvist": för det aktuella (9.0.0) tillståndet för DI och IHttpClientFactory implementeringar finns det ingen enskild ServiceLifetime som skulle vara tillräckligt säker för alla HttpClienti alla möjliga situationerna. Det finns dock en avsikt att ta itu med varningen i de kommande versionerna och byta strategi från "opt-in" till "opt-out".

Gör så här: Avregistrera dig från nyckelregistrering

Du kan aktivt välja bort keyed DI för HttpClientgenom att anropa tilläggsmetoden RemoveAsKeyed, antingen per klientnamn:

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)

Eller "globalt" med 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.

Prioritetsordning

Om de anropas tillsammans eller någon av dem mer än en gång följer AddAsKeyed() och RemoveAsKeyed() vanligtvis reglerna för IHttpClientFactory konfigurationer och DI-registreringar:

  1. Om det anropas med samma namn har den senaste inställningen företräde: livstiden från den senaste AddAsKeyed() används för att skapa nyckelregistrering (såvida inte RemoveAsKeyed() anropades sist, i vilket fall namnet utesluts).
  2. Om den används endast inom ConfigureHttpClientDefaultsvinner den senaste inställningen.
  3. Om både ConfigureHttpClientDefaults och specifika klientnamn användes, anses alla standardvärden inträffa före alla inställningar för varje namn. Därför kan standardvärden ignoreras och den sista av inställningarna per namn vinner.

Se även