IHttpClientFactory mit .NET

In diesem Artikel erfahren Sie, wie Sie mit IHttpClientFactoryHttpClient-Typen mit verschiedenen .NET-Grundlagen wie Dependency Injection (DI), Protokollierung und Konfiguration erstellen können. Der HttpClient-Typ wurde in .NET Framework 4.5 eingeführt, das 2012 veröffentlicht wurde. Anders ausgedrückt: Ihn gibt es schon eine Weile. HttpClient wird zum Senden von HTTP-Anforderungen und zum Behandeln von HTTP-Antworten von Webressourcen verwendet, die von einem Uri identifiziert werden. Das HTTP-Protokoll macht den Großteil des Internetdatenverkehrs aus.

Mit modernen Anwendungsentwicklungsprinzipien, die bewährte Methoden fördern, dient IHttpClientFactory als Factoryabstraktion, die HttpClient-Instanzen mit benutzerdefinierten Konfigurationen erstellen kann. IHttpClientFactory wurde in .NET Core 2.1 eingeführt. Gängige HTTP-basierte .NET-Workloads können die Resilienz und die Behandlung vorübergehender Fehler von Drittanbieter-Middleware problemlos nutzen.

Hinweis

Wenn Ihre App Cookies benötigt, ist es möglicherweise besser, die Verwendung von IHttpClientFactory in Ihrer App zu vermeiden. Alternative Methoden zum Verwalten von Clients finden Sie unter Richtlinien für die Verwendung von HTTP-Clients.

Wichtig

Die Lebensdauerverwaltung von HttpClient-Instanzen, die von IHttpClientFactory erstellt werden, unterscheidet sich vollständig von manuell erstellten Instanzen. Die Strategien sind die Verwendung kurzlebiger Clients, die von IHttpClientFactory erstellt werden, oder langlebiger Clients, für die PooledConnectionLifetime eingerichtet ist. Weitere Informationen finden Sie im Abschnitt HttpClient-Lebensdauerverwaltung und Richtlinien für die Verwendung von HTTP-Clients.

Der Typ IHttpClientFactory

Der ganze Beispielquellencode in diesem Artikel basiert auf den Microsoft.Extensions.Http-NuGet-Paketen. Darüber hinaus werden HTTP-GET-Anforderungen an die kostenlose {JSON}-Platzhalter-API gesendet, um Benutzer-Todo-Objekte abzurufen.

Wenn Sie eine der AddHttpClient-Erweiterungsmethoden aufrufen, fügen Sie die IHttpClientFactory und zugehörige Dienste der IServiceCollection hinzu. Der IHttpClientFactory-Typ bietet folgende Vorteile:

  • Er macht die HttpClient-Klasse als DI-bereiten Typ verfügbar.
  • Ein zentraler Ort für das Benennen und Konfigurieren logischer HttpClient-Instanzen wird damit geboten.
  • Das Konzept der ausgehenden Middleware wird über delegierende Handler in HttpClient in Code umgesetzt.
  • Außerdem werden Erweiterungsmethoden für auf Polly basierende Middleware bereitgestellt, um die delegierenden Handler in HttpClient zu nutzen.
  • Das Zwischenspeichern und die Lebensdauer von zugrunde liegenden HttpClientHandler-Instanzen werden verwaltet. Durch die automatische Verwaltung werden allgemeine DNS-Probleme (Domain Name System) vermieden, die bei der manuellen Verwaltung der Lebensdauer von HttpClient auftreten.
  • Eine konfigurierbare Protokollierungsfunktion wird (über ILogger) für alle Anforderungen hinzugefügt, die über Clients gesendet werden, die von der Factory erstellt wurden.

Verbrauchsmuster

Es gibt mehrere Möglichkeiten IHttpClientFactory in einer App zu verwenden:

Der beste Ansatz richtet sich nach den Anforderungen der App.

Grundlegende Verwendung

Um die IHttpClientFactory zu registrieren, rufen Sie den AddHttpClient auf:

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

Das Nutzen von Diensten kann die IHttpClientFactory als Konstruktorparameter mit DI erfordern. Im folgenden Code wird IHttpClientFactory verwendet, um eine HttpClient-Instanz zu erstellen:

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 [];
    }
}

IHttpClientFactory wie im vorhergehenden Beispiel zu verwenden ist eine gute Möglichkeit zum Umgestalten einer vorhandene App. Dies hat keine Auswirkung auf die Verwendung von HttpClient. An Stellen, an denen HttpClient-Instanzen in einer vorhandenen App erstellt werden, können Sie diese Ereignisse mit Aufrufen von CreateClient ersetzen.

Benannte Clients

Benannte Clients sind in folgenden Fällen eine gute Wahl:

  • Die App erfordert viele verschiedene Verwendungen von HttpClient.
  • Viele HttpClient-Instanzen haben unterschiedliche Konfigurationen.

Die Konfiguration eines benannten HttpClient kann während der Registrierung in der IServiceCollection angegeben werden:

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

Im vorangehenden Code wird der Client mit Folgendem konfiguriert:

  • Ein Name, der aus der Konfiguration unter dem "TodoHttpClientName" gezogen wird.
  • der Basisadresse https://jsonplaceholder.typicode.com/
  • Ein "User-Agent"-Header.

Sie können die Konfiguration verwenden, um HTTP-Clientnamen anzugeben. Dies ist hilfreich, um eine falsche Clientbenennung beim Hinzufügen und Erstellen zu vermeiden. In diesem Beispiel wird die Datei appsettings.json verwendet, um den HTTP-Clientnamen zu konfigurieren:

{
    "TodoHttpClientName": "JsonPlaceholderApi"
}

Es ist einfach, diese Konfiguration zu erweitern und weitere Details zur gewünschten Funktionsweise Ihres HTTP-Clients zu speichern. Weitere Informationen finden Sie unter Konfiguration in .NET.

Erstellen des Clients

Jedes Mal, wenn CreateClient aufgerufen wird, geschieht Folgendes:

  • Eine neue Instanz von HttpClient wird erstellt.
  • Die Konfigurationsaktion wird aufgerufen.

Übergeben Sie für die Erstellung eines benannten Clients dessen Namen an 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 [];
    }
}

Im vorangehenden Code muss die HTTP-Anforderung keinen Hostnamen angeben. Der Code muss nur den Pfad übergeben, da die für den Client konfigurierte Basisadresse verwendet wird.

Typisierte Clients

Typisierte Clients:

  • Stellen dieselben Funktionen wie benannte Clients bereit, ohne Zeichenfolgen als Schlüssel verwenden zu müssen.
  • Bieten IntelliSense- und Compilerhilfe beim Nutzen von Clients.
  • Stellen einen einzelnen Ort zum Konfigurieren und Interagieren mit einem bestimmten HttpClient bereit. Beispielsweise kann ein einzelner typisierter Client für Folgendes verwendet werden:
    • für einen einzelnen Back-End-Endpunkt
    • um die gesamte Logik zu kapseln, die den Endpunkt behandelt
  • Funktionieren mit DI und können überall in der App eingefügt werden, wo sie benötigt werden.

Ein typisierter Client akzeptiert einen HttpClient-Parameter in seinem Konstruktor:

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

Für den Code oben gilt:

  • Die Konfiguration wird festgelegt, wenn der typisierte Client der Dienstsammlung hinzugefügt wird.
  • Der HttpClient wird als Variable im Klassenbereich (Feld) zugewiesen und mit verfügbar gemachten APIs verwendet.

Es können API-spezifische Methoden erstellt werden, die die HttpClient-Funktionalität verfügbar machen. Die GetUserTodosAsync-Methode kapselt z. B. Code zum Abrufen von benutzerspezifischen Todo-Objekten.

Der folgende Code ruft AddHttpClient auf, um eine typisierte Clientklasse zu registrieren:

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

Der typisierte Client wird mit DI als „vorübergehend“ registriert. Im vorherigen Code registriert AddHttpClientTodoService als vorübergehenden Dienst. Diese Registrierung verwendet eine Factorymethode für folgende Aktionen:

  1. Erstellen Sie eine Instanz von HttpClient:
  2. Erstellen einer TodoService-Instanz, wobei die Instanz von HttpClient an ihren Konstrukt übergeben wird

Wichtig

Die Verwendung von typisierten Clients in Singleton-Diensten kann gefährlich sein. Weitere Informationen finden Sie im Abschnitt Vermeiden von typisierten Clients in Singleton-Diensten.

Hinweis

Beim Registrieren eines typisierten Clients mit der AddHttpClient<TClient>-Methode muss der TClient-Typ über einen Konstruktor verfügen, der einen HttpClient-Parameter akzeptiert. Darüber hinaus sollte der TClient-Typ nicht separat beim DI-Container registriert werden.

Benannte und typisierte Clients

Benannte und typisierte Clients haben ihre eigenen Vorteile und Nachteile. Es gibt eine Möglichkeit, diese beiden Clienttypen zu kombinieren, um die Vorteile beider Konzepte nutzen zu können.

Der primäre Anwendungsfall lautet wie folgt: Verwenden Sie denselben typisierten Client, jedoch für unterschiedliche Domänen. Beispielsweise verfügen Sie über einen primären und einen sekundären Dienst, die jeweils exakt dieselbe Funktionalität aufweisen. Dies bedeutet, dass Sie denselben typisierten Client zum Umschließen von HttpClient verwenden können, um Anforderungen auszugeben, Antworten zu verarbeiten und Fehler zu beheben. Derselbe Code wird verwendet, jedoch mit unterschiedlichen Konfigurationen (z. B. unterschiedliche Basisadresse, Timeout und Anmeldeinformationen).

Im folgenden Beispiel wird derselbe typisierte TodoService-Client verwendet, der im Abschnitt Typisierte Clients erläutert wurde.

Registrieren Sie zuerst die benannten und typisierten Clients.

using Shared;
using TypedHttp.Example;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient<TodoService>("primary"
    client =>
    {
        // Configure the primary typed client
        client.BaseAddress = new Uri("https://primary-host-address.com/");
        client.Timeout = TimeSpan.FromSeconds(3);
    });

// Register the same typed client but with different settings
builder.Services.AddHttpClient<TodoService>("secondary"
    client =>
    {
        // Configure the secondary typed client
        client.BaseAddress = new Uri("https://secondary-host-address.com/");
        client.Timeout = TimeSpan.FromSeconds(10);
    });

Im obigen Code:

  • Der erste AddHttpClient-Aufruf registriert einen typisierten TodoService-Client unter dem Namen primary. Das zugrunde liegende HttpClient-Element zeigt auf den primären Dienst und weist ein kurzes Timeout auf.
  • Der zweite AddHttpClient-Aufruf registriert einen typisierten TodoService-Client unter dem Namen secondary. Das zugrunde liegende HttpClient-Element zeigt auf den sekundären Dienst und weist einen längeren Timeout auf.
using IHost host = builder.Build();

// Fetch an IHttpClientFactory instance to create a named client
IHttpClientFactory namedClientFactory =
    host.Services.GetRequiredService<IHttpClientFactory>();

// Fetch an ITypedHttpClientFactory<TodoService> instance to create a named and typed client
ITypedHttpClientFactory<TodoService> typedClientFactory  =
    host.Services.GetRequiredService<ITypedHttpClientFactory<TodoService>>();

// Create a TodoService instance against the primary host
var primaryClient = namedClientFactory.CreateClient("primary");
var todoService = typedClientFactory.CreateClient(primaryClient);

Im obigen Code:

  • Eine IHttpClientFactory-Instanz wird aus dem DI-Container abgerufen, um benannte Clients über die CreateClient-Methode erstellen zu können.
  • Eine ITypedHttpClientFactory<TodoService>-Instanz wird aus dem DI-Container abgerufen, um typisierte Clients über die CreateClient-Methode erstellen zu können.
    • Diese CreateClient-Überladung erhielt ein benanntes HttpClient-Element (mit der richtigen Konfiguration) als Parameter.
    • Das erstellte todoService-Element ist für die Verwendung des primären Diensts konfiguriert.

Hinweis

Der IHttpClientFactory-Typ befindet sich innerhalb der System.Net.Http-Namespaces, während sich der ITypedHttpClientFactory-Typ innerhalb des Microsoft.Extensions.Http-Elements befindet.

Wichtig

Verwenden Sie die Implementierungsklasse (im vorherigen Beispiel TodoService) als Typparameter für ITypedHttpClientFactory. Auch wenn Sie über eine Abstraktion (z. B. ITodoService-Schnittstelle) verfügen, müssen Sie die Implementierung weiterhin verwenden. Wenn Sie versehentlich die Abstraktion (ITodoService) verwenden, wird beim Aufrufen des CreateClient-Elements eine InvalidOperationException ausgelöst.

try
{
    Todo[] todos = await todoService.GetUserTodosAsync(4);
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
    // The request timed out against the primary host

    // Create a TodoService instance against the secondary host
    var fallbackClient = namedClientFactory.CreateClient("secondary");
    var todoFallbackService = typedClientFactory.CreateClient(fallbackClient);

    // Issue request against the secondary host
    Todo[] todos = await todoFallbackService.GetUserTodosAsync(4);
}

Im obigen Code:

  • Es wird versucht, eine Anforderung für den primären Dienst auszustellen.
  • Wenn bei der Anforderung ein Timeout auftritt (dauert länger als drei Sekunden), löst sie eine TaskCanceledException mit einer TimeoutException aus.
  • Bei einem Timeout wird ein neuer Client erstellt und verwendet, der jetzt auf den sekundären Dienst ausgerichtet ist.

Generierte Clients

IHttpClientFactory kann in Verbindung mit Bibliotheken von Drittanbietern verwendet werden, z. B. Refit. Refit ist eine REST-Bibliothek für .NET. Sie ermöglicht deklarative REST-API-Definitionen, die Schnittstellenmethoden Endpunkten zuordnen. Eine Implementierung der Schnittstelle wird dynamisch von RestService generiert. HttpClient wird für die externen HTTP-Aufrufe verwendet.

Berücksichtigen Sie den folgenden record-Typ:

namespace Shared;

public record class Todo(
    int UserId,
    int Id,
    string Title,
    bool Completed);

Das folgende Beispiel basiert auf dem Refit.HttpClientFactory-NuGet-Paket und ist eine einfache Schnittstelle:

using Refit;
using Shared;

namespace GeneratedHttp.Example;

public interface ITodoService
{
    [Get("/todos?userId={userId}")]
    Task<Todo[]> GetUserTodosAsync(int userId);
}

Die vorangehende C#-Schnittstelle:

  • Definiert eine Methode namens GetUserTodosAsync, die eine Task<Todo[]>-Instanz zurückgibt.
  • Deklariert ein Refit.GetAttribute-Attribut mit dem Pfad und der Abfragezeichenfolge für die externe API.

Ein typisierter Client kann hinzugefügt werden. Refit wird zum Generieren der Implementierung verwendet:

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

Die definierte Schnittstelle kann bei Bedarf mit der von DI und Refit bereitgestellten Implementierung verwendet werden.

Stellen von POST-, PUT- und DELETE-Anforderungen

In den vorangehenden Beispielen verwenden alle Anforderungen das GET-HTTP-Verb. HttpClient unterstützt ebenso andere HTTP-Verben, einschließlich der folgenden:

  • POST
  • PUT
  • DELETE
  • PATCH

Eine komplette Liste der unterstützten HTTP-Verben finden Sie unter HttpMethod. Weitere Informationen zum Senden von HTTP-Anforderungen finden Sie unter Senden einer Anforderung mit HttpClient.

Im folgenden Beispiel wird gezeigt, wie Sie eine HTTP-POST-Anforderung vornehmen:

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

Für die CreateItemAsync-Methoden im Code oben gilt Folgendes:

  • Sie serialisiert den Item-Parameter mithilfe von System.Text.Json in JSON. Dabei wird eine Instanz von JsonSerializerOptions verwenden, um den Serialisierungsprozess zu konfigurieren.
  • Sie erstellt eine Instanz von StringContent, um den serialisierten JSON-Code zum Senden im HTTP-Anforderungstext zu packen.
  • Die Methode ruft PostAsync auf, um den JSON-Inhalt an die angegebene URL zu senden. Dies ist eine relative URL, die zu HttpClient.BaseAddress hinzugefügt wird.
  • Sie ruft EnsureSuccessStatusCode auf, um eine Ausnahme auszulösen, wenn der Antwortstatuscode nicht auf Erfolg hinweist.

HttpClient unterstützt auch andere Inhaltstypen. Beispiel: MultipartContent und StreamContent. Eine komplette Liste der unterstützten Inhaltstypen finden Sie unter HttpContent.

Das folgende Beispiel zeigt eine HTTP-PUT-Anforderung:

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

Der vorangehende Code ist dem POST-Beispiel sehr ähnlich. Die UpdateItemAsync-Methode ruft PutAsync anstelle von PostAsync auf.

Das folgende Beispiel zeigt eine HTTP-DELETE-Anforderung:

public async Task DeleteItemAsync(Guid id)
{
    using HttpResponseMessage httpResponse =
        await httpClient.DeleteAsync($"/api/items/{id}");

    httpResponse.EnsureSuccessStatusCode();
}

Im vorangehenden Code ruft die DeleteItemAsync-Methode DeleteAsync auf. Da HTTP-DELETE-Anforderungen normalerweise keinen Text enthalten, stellt die DeleteAsync-Methode keine Überladung bereit, die eine Instanz von HttpContent akzeptiert.

Weitere Informationen zur Verwendung unterschiedlicher HTTP-Verben mit HttpClient finden Sie unter HttpClient.

Verwaltung der HttpClient-Lebensdauer

Bei jedem Aufruf von CreateClient in der IHttpClientFactory wird eine neue Instanz von HttpClient zurückgegeben. Pro Clientname wird eine HttpClientHandler-Instanz erstellt. Die Factory verwaltet die Lebensdauer der HttpClientHandler-Instanzen.

IHttpClientFactory nimmt die Zwischenspeicherung der HttpClientHandler-Instanzen vor, die von der Factory zum Reduzieren des Ressourcenverbrauchs erstellt wurden. Eine HttpClientHandler-Instanz kann aus dem Cache wiederverwendet werden, wenn eine neue HttpClient-Instanz erstellt wird und deren Lebensdauer noch nicht abgelaufen ist.

Das Zwischenspeichern von Handlern ist ein wünschenswerter Vorgang, da jeder Handler in der Regel seinen zugrunde liegenden HTTP-Verbindungspool selbst verwaltet. Die Erstellung von mehr Handlern als nötig kann zu Socketerschöpfung und Verbindungsverzögerungen führen. Einige Handler halten Verbindungen auch unbegrenzt offen, was verhindert, dass der Handler auf DNS-Änderungen reagiert.

Die Standardlebensdauer von Handlern beträgt zwei Minuten. Um den Standardwert zu überschreiben, rufen Sie SetHandlerLifetime für jeden Client für die IServiceCollection auf:

services.AddHttpClient("Named.Client")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Wichtig

HttpClient-Instanzen, die von IHttpClientFactory erstellt werden, sollen kurzlebig sein.

  • Das Recyceln und erneute Erstellen von HttpMessageHandlern nach Ablauf ihrer Lebensdauer ist für IHttpClientFactory unerlässlich, um sicherzustellen, dass Handler auf DNS-Änderungen reagieren. HttpClient ist bei der Erstellung an eine bestimmte Handlerinstanz gebunden, sodass neue HttpClient-Instanzen rechtzeitig angefordert werden sollten, um sicherzustellen, dass der Client den aktualisierten Handler erhält.

  • Die Entsorgung solcher HttpClient-Instanzen, die von der Factory erstellt wurden, führt nicht zu Socketerschöpfung, da ihre Entsorgung keine Entsorgung des HttpMessageHandler auslöst. IHttpClientFactory verfolgt Ressourcen nach und entsorgt diese, die zum Erstellen von HttpClient-Instanzen verwendet werden, insbesondere die HttpMessageHandler-Instanzen, sobald ihre Lebensdauer abläuft und sie nicht mehr von einem HttpClient verwendet werden.

Eine einzelne HttpClient-Instanz über einen langen Zeitraum am Leben zu halten, ist ein gängiges Muster, das als Alternative zu IHttpClientFactory verwendet werden kann. Für dieses Muster ist jedoch zusätzliches Setup erforderlich, z. B. PooledConnectionLifetime. Sie können entweder langlebige Clients mit PooledConnectionLifetime oder kurzlebige Clients verwenden, die von IHttpClientFactory erstellt werden. Informationen dazu, welche Strategie in Ihrer App verwendet werden sollte, finden Sie unter Richtlinien für die Verwendung von HTTP-Clients.

Konfigurieren des HttpMessageHandler

Es kann notwendig sein, die Konfiguration des inneren von einem Client verwendeten HttpMessageHandler zu steuern.

IHttpClientBuilder wird zurückgegeben, wenn benannte oder typisierte Clients hinzugefügt werden. Die Erweiterungsmethode ConfigurePrimaryHttpMessageHandler kann zum Definieren eines Delegaten für die IServiceCollection verwendet werden. Der Delegat wird verwendet, um den primären HttpMessageHandler zu erstellen und konfigurieren, der von dem Client verwendet wird:

.ConfigurePrimaryHttpMessageHandler(() =>
{
    return new HttpClientHandler
    {
        AllowAutoRedirect = false,
        UseDefaultCredentials = true
    };
});

Wenn Sie die HttClientHandler konfigurieren, können Sie einen Proxy für die HttpClient-Instanz unter verschiedenen anderen Eigenschaften des Handlers angeben. Weitere Informationen finden Sie unter Proxy pro Client.

Zusätzliche Konfiguration

Es gibt mehrere zusätzliche Konfigurationsoptionen zum Steuern von IHttpClientHandler:

Methode Beschreibung
AddHttpMessageHandler Fügt einen zusätzlichen Meldungshandler für einen benannten HttpClient hinzu.
AddTypedClient Konfiguriert die Bindung zwischen dem TClient und dem benannten HttpClient, der der IHttpClientBuilder-Methode zugeordnet ist.
ConfigureHttpClient Fügt einen Delegaten hinzu, der für die Konfiguration einer benannten HttpClient-Klasse verwendet wird.
ConfigureHttpMessageHandlerBuilder Fügt einen Delegaten hinzu, der zum Konfigurieren von Meldungshandlern mithilfe von HttpMessageHandlerBuilder der benannten HttpClient-Klasse verwendet wird.
ConfigurePrimaryHttpMessageHandler Konfiguriert die primäre HttpMessageHandler-Klasse aus dem Abhängigkeitsinjektionscontainer für eine benannte HttpClient-Klasse.
RedactLoggedHeaders Legt die Sammlung von HTTP-Headernamen fest, für die vor der Protokollierung Werte bearbeitet werden sollen.
SetHandlerLifetime Legt die Zeitspanne fest, für die eine HttpMessageHandler-Klasse wiederverwendet werden kann. Für jeden benannten Client kann ein eigener Wert für die Lebensdauer des Handlers konfiguriert werden.

Verwenden von IHttpClientFactory zusammen mit SocketsHttpHandler

Die SocketsHttpHandler-Implementierung von HttpMessageHandler wurde in .NET Core 2.1 hinzugefügt, wodurch PooledConnectionLifetime konfiguriert werden kann. Diese Einstellung wird verwendet, um sicherzustellen, dass der Handler auf DNS-Änderungen reagiert. Daher wird die Verwendung von SocketsHttpHandler als Alternative zur Verwendung von IHttpClientFactory betrachtet. Weitere Informationen finden Sie unter Richtlinien für die Verwendung von HTTP-Clients.

SocketsHttpHandler Allerdings können und IHttpClientFactory zusammen verwendet werden, um die Konfigurierbarkeit zu verbessern. Durch die Verwendung dieser beiden APIs profitieren Sie von der Konfigurierbarkeit sowohl auf niedriger Ebene (z. B. bei der Verwendung von LocalCertificateSelectionCallback für die Auswahl dynamischer Zertifikate) als auch auf hoher Ebene (z. B. durch Nutzung von DI-Integration und mehrerer Clientkonfigurationen).

So verwenden Sie beide APIs:

  1. Geben Sie SocketsHttpHandler als PrimaryHandler an, und richten Sie dessen PooledConnectionLifetime ein (z. B. mit einem Wert, der zuvor in HandlerLifetime enthalten war).
  2. Da SocketsHttpHandler Verbindungspooling und Recycling verarbeitet, ist Handlerrecycling auf IHttpClientFactory-Ebene nicht mehr erforderlich. Sie können diese Einstellung deaktivieren, indem Sie HandlerLifetime auf Timeout.InfiniteTimeSpan festlegen.
services.AddHttpClient(name)
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new SocketsHttpHandler()
        {
            PooledConnectionLifetime = TimeSpan.FromMinutes(2)
        };
    })
    .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // Disable rotation, as it is handled by PooledConnectionLifetime

Vermeiden von typisierten Clients in Singleton-Diensten

Bei Verwendung des Ansatzes mit benannten Clients wird IHttpClientFactory in Dienste eingefügt, und HttpClient-Instanzen werden erstellt, indem jedes Mal CreateClient aufgerufen wird, wenn ein HttpClient benötigt wird.

Beim Ansatz mit typisierten Clients sind typisierte Clients jedoch vorübergehende Objekte, die normalerweise in Dienste eingefügt werden. Dies kann zu einem Problem führen, da ein typisierter Client in einen Singleton-Dienst eingefügt werden kann.

Wichtig

Von typisierten Clients wird erwartet, dass sie im gleichen Sinne kurzlebig sind wie HttpClient-Instanzen, die von IHttpClientFactory erstellt werden (weitere Informationen finden Sie unter HttpClient-Lebensdauerverwaltung). Sobald eine typisierte Clientinstanz erstellt wird, besitzt IHttpClientFactory keine Kontrolle über sie. Wenn eine typisierte Clientinstanz in einem Singleton erfasst wird, kann dies verhindern, dass auf DNS-Änderungen reagiert wird, was einen der Zwecke von IHttpClientFactory vereitelt.

Wenn Sie HttpClient-Instanzen in einem Singleton-Dienst verwenden müssen, sollten Sie die folgenden Optionen in Betracht ziehen:

  • Verwenden Sie stattdessen den Ansatz mit einem benannten Client, indem Sie IHttpClientFactory in den Singleton-Dienst einfügen und bei Bedarf HttpClient-Instanzen neu erstellen.
  • Wenn Sie den Ansatz mit einem typisierten Client benötigen, verwenden Sie SocketsHttpHandler mit konfiguriertem PooledConnectionLifetime-Wert als primären Handler. Weitere Informationen zur Verwendung von SocketsHttpHandler mit IHttpClientFactory finden Sie im Abschnitt Verwenden von IHttpClientFactory zusammen mit SocketsHttpHandler.

Nachrichtenhandlerbereiche in IHttpClientFactory

IHttpClientFactory erstellt für jede HttpMessageHandler-Instanz einen separaten DI-Bereich. Diese DI-Bereiche sind von Anwendungs-DI-Bereichen getrennt (z. B. vom ASP.NET-Bereich für eingehende Anforderungen oder einem vom Benutzer erstellten manuellen DI-Bereich), sodass sie keine bereichsbezogenen Dienstinstanzen gemeinsam nutzen. Nachrichtenhandlerbereiche sind an die Lebensdauer des Handlers gebunden und können Anwendungsbereiche überdauern, was beispielsweise dazu führen kann, dass dieselbe HttpMessageHandler-Instanz mit denselben eingefügten Bereichsabhängigkeiten zwischen mehreren eingehenden Anforderungen wiederverwendet wird.

Abbildung: Zwei Anwendungs-DI-Bereiche und ein separater Nachrichtenhandlerbereich

Benutzern wird dringend empfohlen, bereichsbezogene Informationen (z. B. Daten von HttpContext) nicht in HttpMessageHandler-Instanzen zwischenzuspeichern und bereichsbezogene Abhängigkeiten mit Vorsicht zu verwenden, um zu vermeiden, dass vertrauliche Informationen veröffentlicht werden.

Wenn Sie von Ihrem Meldungshandler aus Zugriff auf einen App-DI-Bereich benötigen (beispielsweise zur Authentifizierung), kapseln Sie bereichsbezogene Logik in einem separaten vorübergehenden DelegatingHandler, und umschließen ihn mit einer HttpMessageHandler-Instanz aus dem IHttpClientFactory-Cache. Um auf den Handler zuzugreifen, rufen Sie IHttpMessageHandlerFactory.CreateHandler für jeden registrierten benannten Client auf. In diesem Fall erstellen Sie eine HttpClient-Instanz selbst mithilfe des erstellten Handlers.

Diagramm, das den Zugriff auf die DI-Bereiche der Anwendung über einen separaten transienten Meldungshandler und IHttpMessageHandlerFactory zeigt

Das folgende Beispiel zeigt das Erstellen eines HttpClient mit einem bereichsbezogenen DelegatingHandler:

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

Eine weitere Problemumgehung kann mit einer Erweiterungsmethode für die Registrierung eines bereichsbezogenen DelegatingHandler und einer IHttpClientFactory-Standardregistrierung durch einen vorübergehenden Dienst mit Zugriff auf den aktuellen App-Bereich folgen:

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

Weitere Informationen finden Sie im vollständigen Beispiel.

Siehe auch