Stellen von HTTP-Anforderungen mithilfe von IHttpClientFactory in ASP.NET Core

Von Kirk Larkin, Steve Gordon, Glenn Condron und Ryan Nowak.

IHttpClientFactory kann registriert und zum Konfigurieren und Erstellen von HttpClient-Instanzen in einer App verwendet werden. IHttpClientFactory bietet die folgenden Vorteile:

  • Ein zentraler Ort für das Benennen und Konfigurieren logischer HttpClient-Instanzen wird damit geboten. Beispielsweise kann ein Client namens github registriert und für den Zugriff auf GitHub konfiguriert werden. Ein Standardclient kann für den allgemeinen Zugriff registriert werden.
  • Das Konzept der ausgehenden Middleware wird über delegierende Handler in HttpClient in Code umgesetzt. Außerdem werden Erweiterungen für auf Polly basierende Middleware bereitgestellt, um die delegierenden Handler in HttpClient zu nutzen.
  • Das Pooling und die Lebensdauer von zugrunde liegenden HttpClientMessageHandler-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.

Der Beispielcode in dieser Version des Themas verwendet System.Text.Json, um JSON-Inhalte zu deserialisieren, die in HTTP-Antworten zurückgegeben wurden. Verwenden Sie bei Beispielen, die Json.NET und ReadAsAsync<T> verwenden, die Versionsauswahl, um eine 2.x-Version dieses Themas auszuwählen.

Verbrauchsmuster

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

Der beste Ansatz richtet sich nach den Anforderungen der App.

Grundlegende Verwendung

Registrieren Sie IHttpClientFactory, indem Sie AddHttpClient in Program.cs aufrufen:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddHttpClient();

Eine IHttpClientFactory-Schnittstelle kann mithilfe der Dependency Injection (DI) angefordert werden. Im folgenden Code wird IHttpClientFactory verwendet, um eine HttpClient-Instanz zu erstellen:

public class BasicModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public BasicModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { HeaderNames.Accept, "application/vnd.github.v3+json" },
                { HeaderNames.UserAgent, "HttpRequestsSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}

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 HttpClients verfügen über unterschiedliche Konfigurationen.

Geben Sie die Konfiguration für einen benannten HttpClient bei dessen Registrierung in Program.cs an:

builder.Services.AddHttpClient("GitHub", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // using Microsoft.Net.Http.Headers;
    // The GitHub API requires two headers.
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.Accept, "application/vnd.github.v3+json");
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.UserAgent, "HttpRequestsSample");
});

Im vorangehenden Code wird der Client mit Folgendem konfiguriert:

  • der Basisadresse https://api.github.com/
  • zwei Header, die für die Arbeit mit der GitHub-API erforderlich sind

CreateClient

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:

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public NamedClientModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpClient = _httpClientFactory.CreateClient("GitHub");
        var httpResponseMessage = await httpClient.GetAsync(
            "repos/dotnet/AspNetCore.Docs/branches");

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}

Im vorangehenden Code muss die 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 Hilfe durch IntelliSense und Compiler beim Verarbeiten 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:

public class GitHubService
{
    private readonly HttpClient _httpClient;

    public GitHubService(HttpClient httpClient)
    {
        _httpClient = httpClient;

        _httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    }

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync() =>
        await _httpClient.GetFromJsonAsync<IEnumerable<GitHubBranch>>(
            "repos/dotnet/AspNetCore.Docs/branches");
}

Für den Code oben gilt:

  • Die Konfiguration wird in den typisierten Client verschoben.
  • Die bereitgestellte HttpClient-Instanz wird als privates Feld gespeichert.

Es können API-spezifische Methoden erstellt werden, die die HttpClient-Funktionalität verfügbar machen. Die GetAspNetCoreDocsBranches-Methode kapselt z. B. Code zum Abrufen von GitHub-Branches mit Dokumentation.

Der folgende Code ruft AddHttpClient in Program.cs auf, um die typisierte GitHubService-Clientklasse zu registrieren:

builder.Services.AddHttpClient<GitHubService>();

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

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

Der typisierte Client kann direkt eingefügt und verwendet werden:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public TypedClientModel(GitHubService gitHubService) =>
        _gitHubService = gitHubService;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubService.GetAspNetCoreDocsBranchesAsync();
        }
        catch (HttpRequestException)
        {
            // ...
        }
    }
}

Die Konfiguration kann für einen typisierten Client auch während dessen Registrierung in Program.cs angegeben werden, anstatt im Konstruktor des typisierten Clients:

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // ...
});

Generierte Clients

IHttpClientFactory kann in Verbindung mit Bibliotheken von Drittanbietern verwendet werden, z. B. Refit. Refit ist eine REST-Bibliothek für .NET. Sie konvertiert REST-APIs in Live-Schnittstellen. Rufen Sie AddRefitClient auf, um eine dynamische Implementierung einer Schnittstelle zu generieren, die HttpClient zum Durchführen der externen HTTP-Aufrufe verwendet.

Eine benutzerdefinierte Schnittstelle stellt die externe API dar:

public interface IGitHubClient
{
    [Get("/repos/dotnet/AspNetCore.Docs/branches")]
    Task<IEnumerable<GitHubBranch>> GetAspNetCoreDocsBranchesAsync();
}

Rufen Sie AddRefitClient auf, um die dynamische Implementierung zu generieren, und rufen Sie dann ConfigureHttpClient auf, um den zugrunde liegenden HttpClient zu konfigurieren:

builder.Services.AddRefitClient<IGitHubClient>()
    .ConfigureHttpClient(httpClient =>
    {
        httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    });

Verwenden Sie DI, um auf die dynamische Implementierung von IGitHubClient zuzugreifen:

public class RefitModel : PageModel
{
    private readonly IGitHubClient _gitHubClient;

    public RefitModel(IGitHubClient gitHubClient) =>
        _gitHubClient = gitHubClient;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubClient.GetAspNetCoreDocsBranchesAsync();
        }
        catch (ApiException)
        {
            // ...
        }
    }
}

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.

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

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json); // using static System.Net.Mime.MediaTypeNames;

    using var httpResponseMessage =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponseMessage.EnsureSuccessStatusCode();
}

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

  • Sie serialisiert den TodoItem-Parameter mithilfe von System.Text.Json in JSON.
  • 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 SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json);

    using var httpResponseMessage =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponseMessage.EnsureSuccessStatusCode();
}

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

Das folgende Beispiel zeigt eine HTTP-DELETE-Anforderung.

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponseMessage =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponseMessage.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.

Middleware für ausgehende Anforderungen

HttpClient enthält das Konzept, Handler zu delegieren, die für ausgehende HTTP-Anforderungen miteinander verknüpft werden können. IHttpClientFactory:

  • Erleichtert das Definieren der Handler, die für jeden benannten Client angewendet werden
  • Unterstützt die Registrierung und Verkettung von mehreren Handlern, um eine Pipeline für die Middleware für ausgehende Anforderungen zu erstellen. Jeder dieser Handler kann vor und nach der ausgehenden Anforderung Aufgaben ausführen. Dieses Muster:
    • ähnelt der eingehenden Pipeline für Middleware in ASP.NET Core
    • bietet einen Mechanismus zum Verwalten von übergreifenden Belangen bezüglich HTTP-Anforderungen, z. B.:
      • Zwischenspeicherung
      • Fehlerbehandlung
      • Serialisierung
      • Protokollierung

So erstellen Sie einen delegierenden Handler:

  • Leiten Sie von DelegatingHandler ab.
  • Überschreiben Sie SendAsync. Führen Sie Code aus, bevor die Anforderung an den nächsten Handler in der Pipeline übergeben wird:
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "The API key header X-API-KEY is required.")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Der vorangehende Code überprüft, ob die Anforderung einen X-API-KEY-Header enthält. Wenn X-API-KEY fehlt, wird BadRequest zurückgegeben.

Für eine HttpClient-Klasse mit Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler kann mehr als ein Handler zur Konfiguration hinzugefügt werden:

builder.Services.AddTransient<ValidateHeaderHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
    .AddHttpMessageHandler<ValidateHeaderHandler>();

Im vorangehenden Code ist ValidateHeaderHandler mit DI registriert. Nach der Registrierung kann AddHttpMessageHandler aufgerufen werden, was den Typ für den Handler übergibt.

Mehrere Handler können in der Reihenfolge registriert werden, in der sie ausgeführt werden sollen. Jeder Handler umschließt den nächsten Handler, bis der endgültige HttpClientHandler die Anforderung ausführt:

builder.Services.AddTransient<SampleHandler1>();
builder.Services.AddTransient<SampleHandler2>();

builder.Services.AddHttpClient("MultipleHttpMessageHandlers")
    .AddHttpMessageHandler<SampleHandler1>()
    .AddHttpMessageHandler<SampleHandler2>();

Im vorangehenden Code wird SampleHandler1 zuerst ausgeführt, vor SampleHandler2.

Verwenden von DI in Middleware für ausgehende Anforderungen

Wenn IHttpClientFactory einen neuen delegierenden Handler erstellt, wird DI verwendet, um die Konstruktorparameter des Handlers zu erfüllen. IHttpClientFactory erstellt einen separaten DI-Bereich für jeden Handler. Dies kann zu überraschendem Verhalten führen, wenn ein Handler einen bereichsbezogenen Dienst nutzt.

Sehen Sie sich beispielsweise die folgende Schnittstelle und ihre Implementierung an, die eine Aufgabe als Vorgang mit einem Bezeichner OperationId darstellt:

public interface IOperationScoped
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Wie der Name bereits vermuten lässt, wird IOperationScoped bei DI registriert, wobei eine bereichsbezogene Lebensdauer verwendet wird:

builder.Services.AddScoped<IOperationScoped, OperationScoped>();

Der folgende delegierende Handler verwendet IOperationScoped, um den X-OPERATION-ID-Header für die ausgehende Anforderung festzulegen:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationScoped;

    public OperationHandler(IOperationScoped operationScoped) =>
        _operationScoped = operationScoped;

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationScoped.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

Navigieren Sie im HttpRequestsSample-Download zu /Operation, und aktualisieren Sie die Seite. Der Wert für den Anforderungsbereich ändert sich für jede Anforderung, aber der Wert des Handlerbereichs ändert sich nur alle 5 Sekunden.

Handler können von Diensten eines beliebigen Bereichs abhängen. Dienste, von denen Handler abhängig sind, werden verworfen, wenn der Handler verworfen wird.

Verwenden Sie einen der folgenden Ansätze, um den anforderungsspezifischen Zustand mit Meldungshandlern zu teilen:

Verwenden von Polly-basierten Handlern

IHttpClientFactory ist mit der Drittanbieterbibliothek Polly integriert. Polly ist eine umfassende Bibliothek für die Behandlung von beständigen und vorübergehenden Fehlern für .NET. Entwicklern wird damit ermöglicht, Richtlinien wie Wiederholungsrichtlinien, Trennschalterrichtlinien, Timeout-Richtlinien, Bulkhead Isolation-Richtlinien und Ausweichrichtlinien in einer flüssigen und threadsicheren Art und Weise auszudrücken.

Erweiterungsmethoden werden bereitgestellt, um die Verwendung von Polly-Richtlinien mit konfigurierten HttpClient-Instanzen zu aktivieren. Die Polly-Erweiterungen unterstützen das Hinzufügen von Polly-basierten Handlern zu Clients. Polly erfordert das Microsoft.Extensions.Http.Polly-NuGet-Paket.

Behandeln von vorrübergehenden Fehlern

Fehler treten normalerweise auf, wenn externe HTTP-Aufrufe vorübergehend sind. AddTransientHttpErrorPolicy ermöglicht die Definition einer Richtlinie, um vorübergehende Fehler zu behandeln. Mit AddTransientHttpErrorPolicy konfigurierte Richtlinien behandeln die folgenden Antworten:

AddTransientHttpErrorPolicy ermöglicht den Zugriff auf ein PolicyBuilder-Objekt, das für die Fehlerbehandlung konfiguriert wurde und mögliche vorübergehende Fehler darstellt:

builder.Services.AddHttpClient("PollyWaitAndRetry")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.WaitAndRetryAsync(
            3, retryNumber => TimeSpan.FromMilliseconds(600)));

Im vorangehenden Code wird eine WaitAndRetryAsync-Richtlinie definiert. Anforderungsfehler werden bis zu dreimal mit einer Verzögerung von 600 ms wiederholt.

Dynamisches Auswählen von Richtlinien

Erweiterungsmethoden werden zum Hinzufügen von Polly-basierten Handlern bereitgestellt, z. B. AddPolicyHandler. Der folgende AddPolicyHandler-Überladung prüft die Anforderung, um zu entscheiden, welche Richtlinie angewendet werden soll:

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

builder.Services.AddHttpClient("PollyDynamic")
    .AddPolicyHandler(httpRequestMessage =>
        httpRequestMessage.Method == HttpMethod.Get ? timeoutPolicy : longTimeoutPolicy);

Im vorangehenden Code wird ein 10 Sekunden langer Timeout angewendet, wenn die ausgehende Anforderung eine HTTP GET-Anforderung ist. Für alle anderen HTTP-Methoden wird ein 30 Sekunden langer Timeout verwendet.

Hinzufügen mehrerer Polly-Handler

Es ist üblich, Polly-Richtlinien zu schachteln:

builder.Services.AddHttpClient("PollyMultiple")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.RetryAsync(3))
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

Im vorherigen Beispiel:

  • Zwei Handler werden hinzugefügt.
  • Der erste Handler verwendet AddTransientHttpErrorPolicy, um eine Wiederholungsrichtlinie hinzuzufügen. Fehlgeschlagene Anforderungen werden bis zu drei Mal wiederholt.
  • Der zweite AddTransientHttpErrorPolicy-Aufruf fügt eine Trennschalterrichtlinie hinzu. Weitere externe Anforderungen werden 30 Sekunden lang blockiert, wenn fünf Fehlversuche hintereinander stattfinden. Trennschalterrichtlinien sind zustandsbehaftet. Alle Aufrufe über diesen Client teilen den gleichen Schalterzustand.

Hinzufügen von Richtlinien aus der Polly-Registrierung

Eine Methode zum Verwalten regelmäßig genutzter Richtlinien ist, Sie einmal zu definieren und mit PolicyRegistry zu registrieren. Beispiel:

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

var policyRegistry = builder.Services.AddPolicyRegistry();

policyRegistry.Add("Regular", timeoutPolicy);
policyRegistry.Add("Long", longTimeoutPolicy);

builder.Services.AddHttpClient("PollyRegistryRegular")
    .AddPolicyHandlerFromRegistry("Regular");

builder.Services.AddHttpClient("PollyRegistryLong")
    .AddPolicyHandlerFromRegistry("Long");

Für den Code oben gilt:

  • Der Polly-Registrierung werden zwei Richtlinien hinzugefügt: Regular und Long.
  • AddPolicyHandlerFromRegistry konfiguriert einzelne benannte Clients für die Verwendung dieser Richtlinien aus der Polly-Registrierung.

Weitere Informationen zu IHttpClientFactory und Polly-Integrationen finden Sie im Polly-Wiki.

HttpClient und die Verwaltung der Lebensdauer

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

IHttpClientFactory legt die HttpMessageHandler-Instanzen zusammen, die von der Factory zum Reduzieren des Ressourcenverbrauchs erstellt wurden. Eine HttpMessageHandler-Instanz kann aus dem Pool wiederverwendet werden, wenn eine neue HttpClient-Instanz erstellt wird und deren Lebensdauer noch nicht abgelaufen ist.

Das Zusammenlegen von Handlern ist ein wünschenswerter Vorgang, da jeder Handler in der Regel seine zugrunde liegenden HTTP-Verbindungen selbst verwaltet. Wenn mehr Handler als nötig erstellt werden, können Verzögerungen bei Verbindungen entstehen. Einige Handler halten Verbindungen auch unbegrenzt offen, was verhindert, dass der Handler auf DNS-Änderungen (Domain Name System) reagiert.

Die Standardlebensdauer von Handlern beträgt zwei Minuten. Der Standardwert kann für jeden benannten Client überschrieben werden:

builder.Services.AddHttpClient("HandlerLifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

HttpClient-Instanzen können im Allgemeinen als .NET-Objekte behandelt werden, die nicht verworfen werden müssen. Beim Verwerfen werden ausgehende Anforderungen abgebrochen, und es wird sichergestellt, dass die angegebene HttpClient-Instanz nach dem Aufruf von Dispose nicht mehr verwendet werden kann. IHttpClientFactory verfolgt von HttpClient-Instanzen verwendete Ressourcen nach und verwirft sie.

Das Beibehalten einer einzelnen HttpClient-Instanz für einen langen Zeitraum ist ein allgemeines Muster, das vor der Einführung von IHttpClientFactory verwendet wurde. Dieses Muster wird nach der Migration zu IHttpClientFactory überflüssig.

Alternativen zu IHttpClientFactory

Mit der Verwendung von IHttpClientFactory in einer DI-fähigen App wird Folgendes vermieden:

  • Probleme mit der Ressourcenauslastung durch Zusammenlegen von HttpMessageHandler-Instanzen
  • Probleme durch veraltetes DNS durch regelmäßigen Wechsel von HttpMessageHandler-Instanzen

Es gibt alternative Möglichkeiten zum Lösen des vorangehenden Problems mithilfe einer langlebigen SocketsHttpHandler-Instanz:

  • Erstellen Sie eine SocketsHttpHandler-Instanz, wenn die App gestartet wird, und verwenden Sie diese für die Lebensdauer der App.
  • Konfigurieren Sie einen geeigneten Wert für PooledConnectionLifetime basierend auf den DNS-Aktualisierungszeiten.
  • Erstellen Sie bei Bedarf HttpClient-Instanzen mithilfe von new HttpClient(handler, disposeHandler: false).

Diese Ansätze lösen die Ressourcenverwaltungsprobleme, die IHttpClientFactory auf ähnliche Weise löst.

  • SocketsHttpHandler gibt Verbindungen für HttpClient-Instanzen frei. Durch diese Freigabe wird die Erschöpfung von Sockets vermieden.
  • SocketsHttpHandler wechselt die Verbindungen anhand von PooledConnectionLifetime, um Probleme durch veraltetes DNS zu umgehen.

Protokollierung

Über IHttpClientFactory erstellte Clients zeichnen Protokollmeldungen für alle Anforderungen auf. Aktivieren Sie die entsprechende Informationsebene in der Protokollierungskonfiguration, um die Standardprotokollmeldungen anzuzeigen. Zusätzliche Protokollierung, z.B. das Protokollieren von Anforderungsheadern, wird nur auf der Ablaufverfolgungsebene enthalten.

Die Protokollierungskategorie für jeden Client enthält den Namen des Clients. Ein Client namens MyNamedClient protokolliert z. B. Nachrichten mit der Kategorie „System.Net.Http.HttpClient.MyNamedClient.LogicalHandler“. Meldungen mit dem Suffix LogicalHandler treten außerhalb der Anforderungshandlerpipeline auf. In der Anforderung werden Meldungen protokolliert, bevor andere Handler in der Pipeline sie verarbeitet haben. In der Antwort werden Meldungen protokolliert, nachdem andere Handler in der Pipeline die Antwort empfangen haben.

Die Protokollierung tritt ebenfalls innerhalb der Anforderungshandlerpipeline auf. Im Beispiel von MyNamedClient werden diese Nachrichten mit der Protokollkategorie „System.Net.Http.HttpClient.MyNamedClient.ClientHandler“ protokolliert. Für die Anforderung erfolgt dies, nachdem alle anderen Handler ausgeführt wurden und bevor die Anforderung gesendet wird. Diese Protokollierung enthält den Zustand der Antwort in der Antwort, bevor sie an die Handlerpipeline zurückgegeben wird.

Das Aktivieren der Protokollierung außerhalb und innerhalb der Pipeline ermöglicht die Überprüfung der Änderungen, die durch andere Handler in der Pipeline erfolgt sind. Dies kann die Änderungen an Anforderungsheadern oder am Statuscode der Antwort enthalten.

Das Einschließen des Namens des Clients in der Protokollkategorie aktiviert die Protokollfilterung für spezifische benannte Clients.

Konfigurieren von 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 verwendet werden. Der Delegat wird verwendet, um den primären HttpMessageHandler zu erstellen und konfigurieren, der von dem Client verwendet wird:

builder.Services.AddHttpClient("ConfiguredHttpMessageHandler")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            AllowAutoRedirect = true,
            UseDefaultCredentials = true
        });

Cookie

Die zusammengelegten HttpMessageHandler-Instanzen resultieren in der Freigabe von CookieContainer-Objekten. Die unerwartete Freigabe von CookieContainer-Objekten führt oft zu fehlerhaftem Code. Ziehen Sie für Apps, die cookies erfordern, folgende Vorgehensweisen in Betracht:

  • Deaktivieren der automatischen cookieverarbeitung
  • Vermeiden der Verwendung von IHttpClientFactory

Rufen Sie ConfigurePrimaryHttpMessageHandler auf, um die automatische cookieverarbeitung zu deaktivieren:

builder.Services.AddHttpClient("NoAutomaticCookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            UseCookies = false
        });

Verwenden von IHttpClientFactory in einer Konsolen-App

Fügen Sie in einer Konsolen-App dem Projekt die folgenden Paketverweise hinzu:

Im folgenden Beispiel:

  • IHttpClientFactory und GitHubService sind im Dienstcontainer des generischen Hosts registriert.
  • GitHubService wird über DI angefordert, die wiederum eine Instanz von IHttpClientFactory anfordert.
  • GitHubService erstellt über IHttpClientFactory eine Instanz von HttpClient, die verwendet wird, um GitHub-Branches mit Dokumentation abzurufen.
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var host = new HostBuilder()
    .ConfigureServices(services =>
    {
        services.AddHttpClient();
        services.AddTransient<GitHubService>();
    })
    .Build();

try
{
    var gitHubService = host.Services.GetRequiredService<GitHubService>();
    var gitHubBranches = await gitHubService.GetAspNetCoreDocsBranchesAsync();

    Console.WriteLine($"{gitHubBranches?.Count() ?? 0} GitHub Branches");

    if (gitHubBranches is not null)
    {
        foreach (var gitHubBranch in gitHubBranches)
        {
            Console.WriteLine($"- {gitHubBranch.Name}");
        }
    }
}
catch (Exception ex)
{
    host.Services.GetRequiredService<ILogger<Program>>()
        .LogError(ex, "Unable to load branches from GitHub.");
}

public class GitHubService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public GitHubService(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { "Accept", "application/vnd.github.v3+json" },
                { "User-Agent", "HttpRequestsConsoleSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        httpResponseMessage.EnsureSuccessStatusCode();

        using var contentStream =
            await httpResponseMessage.Content.ReadAsStreamAsync();
        
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubBranch>>(contentStream);
    }
}

public record GitHubBranch(
    [property: JsonPropertyName("name")] string Name);

Middleware für Headerweitergabe

Für die Headerweitergabe wird eine ASP.NET Core-Middleware verwendet, um HTTP-Header von eingehenden Anforderungen an ausgehende HttpClient-Anforderungen weiterzugeben. So verwenden Sie die Headerweitergabe:

  • Installieren Sie das Paket Microsoft.AspNetCore.HeaderPropagation.

  • Konfigurieren Sie den HttpClient und die Middlewarepipeline in Program.cs:

    // Add services to the container.
    builder.Services.AddControllers();
    
    builder.Services.AddHttpClient("PropagateHeaders")
        .AddHeaderPropagation();
    
    builder.Services.AddHeaderPropagation(options =>
    {
        options.Headers.Add("X-TraceId");
    });
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    app.UseHttpsRedirection();
    
    app.UseHeaderPropagation();
    
    app.MapControllers();
    
  • Erstellen Sie ausgehende Anforderungen mithilfe der konfigurierten HttpClient-Instanz, die die hinzugefügten Header enthält.

Zusätzliche Ressourcen

Von Kirk Larkin, Steve Gordon, Glenn Condron und Ryan Nowak.

IHttpClientFactory kann registriert und zum Konfigurieren und Erstellen von HttpClient-Instanzen in einer App verwendet werden. IHttpClientFactory bietet die folgenden Vorteile:

  • Ein zentraler Ort für das Benennen und Konfigurieren logischer HttpClient-Instanzen wird damit geboten. Beispielsweise kann ein Client namens github registriert und für den Zugriff auf GitHub konfiguriert werden. Ein Standardclient kann für den allgemeinen Zugriff registriert werden.
  • Das Konzept der ausgehenden Middleware wird über delegierende Handler in HttpClient in Code umgesetzt. Außerdem werden Erweiterungen für auf Polly basierende Middleware bereitgestellt, um die delegierenden Handler in HttpClient zu nutzen.
  • Das Pooling und die Lebensdauer von zugrunde liegenden HttpClientMessageHandler-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.

Zeigen Sie Beispielcode an, oder laden Sie diesen herunter (Vorgehensweise zum Herunterladen).

Der Beispielcode in dieser Version des Themas verwendet System.Text.Json, um JSON-Inhalte zu deserialisieren, die in HTTP-Antworten zurückgegeben wurden. Verwenden Sie bei Beispielen, die Json.NET und ReadAsAsync<T> verwenden, die Versionsauswahl, um eine 2.x-Version dieses Themas auszuwählen.

Verbrauchsmuster

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

Der beste Ansatz richtet sich nach den Anforderungen der App.

Grundlegende Verwendung

IHttpClientFactory kann durch Aufrufen von AddHttpClient registriert werden:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

Eine IHttpClientFactory-Schnittstelle kann mithilfe der Dependency Injection (DI) angefordert werden. Im folgenden Code wird IHttpClientFactory verwendet, um eine HttpClient-Instanz zu erstellen:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

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 HttpClients verfügen über unterschiedliche Konfigurationen.

Die Konfiguration eines benannten HttpClient kann während der Registrierung in Startup.ConfigureServices angegeben werden:

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

Im vorangehenden Code wird der Client mit Folgendem konfiguriert:

  • der Basisadresse https://api.github.com/
  • zwei Header, die für die Arbeit mit der GitHub-API erforderlich sind

CreateClient

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:

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

Im vorangehenden Code muss die 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 Hilfe durch IntelliSense und Compiler beim Verarbeiten 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:

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        return await Client.GetFromJsonAsync<IEnumerable<GitHubIssue>>(
          "/repos/aspnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");
    }
}

Für den Code oben gilt:

  • Die Konfiguration wird in den typisierten Client verschoben.
  • Das HttpClient-Objekt wird als öffentliche Eigenschaft zur Verfügung gestellt.

Es können API-spezifische Methoden erstellt werden, die die HttpClient-Funktionalität verfügbar machen. Die GetAspNetDocsIssues-Methode kapselt z. B. Code zum Abrufen offener Probleme.

Der folgende Code ruft AddHttpClient in Startup.ConfigureServices auf, um eine typisierte Clientklasse zu registrieren:

services.AddHttpClient<GitHubService>();

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

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

Der typisierte Client kann direkt eingefügt und verwendet werden:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

Die Konfiguration kann für einen typisierten Client während der Registrierung in Startup.ConfigureServices angegeben werden, anstatt im Konstruktor des typisierten Clients:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

HttpClient kann in einem typisierten Client gekapselt werden. Anstatt ihn als eine Eigenschaft zur Verfügung zu stellen, definieren Sie eine Methode, die die HttpClient-Instanz intern aufruft:

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

Im vorangehenden Code wird HttpClient in einem privaten Feld gespeichert. Der Zugriff auf HttpClient erfolgt durch die öffentliche GetRepos-Methode.

Generierte Clients

IHttpClientFactory kann in Verbindung mit Bibliotheken von Drittanbietern verwendet werden, z. B. Refit. Refit ist eine REST-Bibliothek für .NET. Sie konvertiert REST-APIs in Live-Schnittstellen. Eine Implementierung der Schnittstelle wird dynamisch von RestService generiert. HttpClient wird für die externen HTTP-Aufrufe verwendet.

Eine Schnittstelle und eine Antwort werden definiert, um die externe API und die Antwort darzustellen:

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

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

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

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

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

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.

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

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

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

  • Sie serialisiert den TodoItem-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 SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

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

Das folgende Beispiel zeigt eine HTTP-DELETE-Anforderung.

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    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.

Middleware für ausgehende Anforderungen

HttpClient enthält das Konzept, Handler zu delegieren, die für ausgehende HTTP-Anforderungen miteinander verknüpft werden können. IHttpClientFactory:

  • Erleichtert das Definieren der Handler, die für jeden benannten Client angewendet werden
  • Unterstützt die Registrierung und Verkettung von mehreren Handlern, um eine Pipeline für die Middleware für ausgehende Anforderungen zu erstellen. Jeder dieser Handler kann vor und nach der ausgehenden Anforderung Aufgaben ausführen. Dieses Muster:
    • ähnelt der eingehenden Pipeline für Middleware in ASP.NET Core
    • bietet einen Mechanismus zum Verwalten von übergreifenden Belangen bezüglich HTTP-Anforderungen, z. B.:
      • Zwischenspeicherung
      • Fehlerbehandlung
      • Serialisierung
      • Protokollierung

So erstellen Sie einen delegierenden Handler:

  • Leiten Sie von DelegatingHandler ab.
  • Überschreiben Sie SendAsync. Führen Sie Code aus, bevor die Anforderung an den nächsten Handler in der Pipeline übergeben wird:
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Der vorangehende Code überprüft, ob die Anforderung einen X-API-KEY-Header enthält. Wenn X-API-KEY fehlt, wird BadRequest zurückgegeben.

Für eine HttpClient-Klasse mit Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler kann mehr als ein Handler zur Konfiguration hinzugefügt werden:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

Im vorangehenden Code ist ValidateHeaderHandler mit DI registriert. Nach der Registrierung kann AddHttpMessageHandler aufgerufen werden, was den Typ für den Handler übergibt.

Mehrere Handler können in der Reihenfolge registriert werden, in der sie ausgeführt werden sollen. Jeder Handler umschließt den nächsten Handler, bis der endgültige HttpClientHandler die Anforderung ausführt:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Verwenden von DI in Middleware für ausgehende Anforderungen

Wenn IHttpClientFactory einen neuen delegierenden Handler erstellt, wird DI verwendet, um die Konstruktorparameter des Handlers zu erfüllen. IHttpClientFactory erstellt einen separaten DI-Bereich für jeden Handler. Dies kann zu überraschendem Verhalten führen, wenn ein Handler einen bereichsbezogenen Dienst nutzt.

Sehen Sie sich beispielsweise die folgende Schnittstelle und ihre Implementierung an, die eine Aufgabe als Vorgang mit einem Bezeichner OperationId darstellt:

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Wie der Name bereits vermuten lässt, wird IOperationScoped bei DI registriert, wobei eine bereichsbezogene Lebensdauer verwendet wird:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

Der folgende delegierende Handler verwendet IOperationScoped, um den X-OPERATION-ID-Header für die ausgehende Anforderung festzulegen:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

Navigieren Sie im HttpRequestsSample-Download] zu /Operation, und aktualisieren Sie die Seite. Der Wert für den Anforderungsbereich ändert sich für jede Anforderung, aber der Wert des Handlerbereichs ändert sich nur alle 5 Sekunden.

Handler können von Diensten eines beliebigen Bereichs abhängen. Dienste, von denen Handler abhängig sind, werden verworfen, wenn der Handler verworfen wird.

Verwenden Sie einen der folgenden Ansätze, um den anforderungsspezifischen Zustand mit Meldungshandlern zu teilen:

Verwenden von Polly-basierten Handlern

IHttpClientFactory ist mit der Drittanbieterbibliothek Polly integriert. Polly ist eine umfassende Bibliothek für die Behandlung von beständigen und vorübergehenden Fehlern für .NET. Entwicklern wird damit ermöglicht, Richtlinien wie Wiederholungsrichtlinien, Trennschalterrichtlinien, Timeout-Richtlinien, Bulkhead Isolation-Richtlinien und Ausweichrichtlinien in einer flüssigen und threadsicheren Art und Weise auszudrücken.

Erweiterungsmethoden werden bereitgestellt, um die Verwendung von Polly-Richtlinien mit konfigurierten HttpClient-Instanzen zu aktivieren. Die Polly-Erweiterungen unterstützen das Hinzufügen von Polly-basierten Handlern zu Clients. Polly erfordert das Microsoft.Extensions.Http.Polly-NuGet-Paket.

Behandeln von vorrübergehenden Fehlern

Fehler treten normalerweise auf, wenn externe HTTP-Aufrufe vorübergehend sind. AddTransientHttpErrorPolicy ermöglicht die Definition einer Richtlinie, um vorübergehende Fehler zu behandeln. Mit AddTransientHttpErrorPolicy konfigurierte Richtlinien behandeln die folgenden Antworten:

AddTransientHttpErrorPolicy ermöglicht den Zugriff auf ein PolicyBuilder-Objekt, das für die Fehlerbehandlung konfiguriert wurde und mögliche vorübergehende Fehler darstellt:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

Im vorangehenden Code wird eine WaitAndRetryAsync-Richtlinie definiert. Anforderungsfehler werden bis zu dreimal mit einer Verzögerung von 600 ms wiederholt.

Dynamisches Auswählen von Richtlinien

Erweiterungsmethoden werden zum Hinzufügen von Polly-basierten Handlern bereitgestellt, z. B. AddPolicyHandler. Der folgende AddPolicyHandler-Überladung prüft die Anforderung, um zu entscheiden, welche Richtlinie angewendet werden soll:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

Im vorangehenden Code wird ein 10 Sekunden langer Timeout angewendet, wenn die ausgehende Anforderung eine HTTP GET-Anforderung ist. Für alle anderen HTTP-Methoden wird ein 30 Sekunden langer Timeout verwendet.

Hinzufügen mehrerer Polly-Handler

Es ist üblich, Polly-Richtlinien zu schachteln:

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

Im vorherigen Beispiel:

  • Zwei Handler werden hinzugefügt.
  • Der erste Handler verwendet AddTransientHttpErrorPolicy, um eine Wiederholungsrichtlinie hinzuzufügen. Fehlgeschlagene Anforderungen werden bis zu drei Mal wiederholt.
  • Der zweite AddTransientHttpErrorPolicy-Aufruf fügt eine Trennschalterrichtlinie hinzu. Weitere externe Anforderungen werden 30 Sekunden lang blockiert, wenn fünf Fehlversuche hintereinander stattfinden. Trennschalterrichtlinien sind zustandsbehaftet. Alle Aufrufe über diesen Client teilen den gleichen Schalterzustand.

Hinzufügen von Richtlinien aus der Polly-Registrierung

Eine Methode zum Verwalten regelmäßig genutzter Richtlinien ist, Sie einmal zu definieren und mit PolicyRegistry zu registrieren.

Im folgenden Code wird Folgendes ausgeführt:

  • Die Richtlinien „regular“ und „long“ werden hinzugefügt.
  • AddPolicyHandlerFromRegistry fügt die Richtlinien „regular“ und „long“ aus der Registrierung hinzu.
public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

Weitere Informationen zu IHttpClientFactory und Polly-Integrationen finden Sie im Polly-Wiki.

HttpClient und die Verwaltung der Lebensdauer

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

IHttpClientFactory legt die HttpMessageHandler-Instanzen zusammen, die von der Factory zum Reduzieren des Ressourcenverbrauchs erstellt wurden. Eine HttpMessageHandler-Instanz kann aus dem Pool wiederverwendet werden, wenn eine neue HttpClient-Instanz erstellt wird und deren Lebensdauer noch nicht abgelaufen ist.

Das Zusammenlegen von Handlern ist ein wünschenswerter Vorgang, da jeder Handler in der Regel seine zugrunde liegenden HTTP-Verbindungen selbst verwaltet. Wenn mehr Handler als nötig erstellt werden, können Verzögerungen bei Verbindungen entstehen. Einige Handler halten Verbindungen auch unbegrenzt offen, was verhindert, dass der Handler auf DNS-Änderungen (Domain Name System) reagiert.

Die Standardlebensdauer von Handlern beträgt zwei Minuten. Der Standardwert kann für jeden benannten Client überschrieben werden:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

HttpClient-Instanzen können im Allgemeinen als .NET-Objekte behandelt werden, die nicht verworfen werden müssen. Beim Verwerfen werden ausgehende Anforderungen abgebrochen, und es wird sichergestellt, dass die angegebene HttpClient-Instanz nach dem Aufruf von Dispose nicht mehr verwendet werden kann. IHttpClientFactory verfolgt von HttpClient-Instanzen verwendete Ressourcen nach und verwirft sie.

Das Beibehalten einer einzelnen HttpClient-Instanz für einen langen Zeitraum ist ein allgemeines Muster, das vor der Einführung von IHttpClientFactory verwendet wurde. Dieses Muster wird nach der Migration zu IHttpClientFactory überflüssig.

Alternativen zu IHttpClientFactory

Mit der Verwendung von IHttpClientFactory in einer DI-fähigen App wird Folgendes vermieden:

  • Probleme mit der Ressourcenauslastung durch Zusammenlegen von HttpMessageHandler-Instanzen
  • Probleme durch veraltetes DNS durch regelmäßigen Wechsel von HttpMessageHandler-Instanzen

Es gibt alternative Möglichkeiten zum Lösen des vorangehenden Problems mithilfe einer langlebigen SocketsHttpHandler-Instanz:

  • Erstellen Sie eine SocketsHttpHandler-Instanz, wenn die App gestartet wird, und verwenden Sie diese für die Lebensdauer der App.
  • Konfigurieren Sie einen geeigneten Wert für PooledConnectionLifetime basierend auf den DNS-Aktualisierungszeiten.
  • Erstellen Sie bei Bedarf HttpClient-Instanzen mithilfe von new HttpClient(handler, disposeHandler: false).

Diese Ansätze lösen die Ressourcenverwaltungsprobleme, die IHttpClientFactory auf ähnliche Weise löst.

  • SocketsHttpHandler gibt Verbindungen für HttpClient-Instanzen frei. Durch diese Freigabe wird die Erschöpfung von Sockets vermieden.
  • SocketsHttpHandler wechselt die Verbindungen anhand von PooledConnectionLifetime, um Probleme durch veraltetes DNS zu umgehen.

Cookie

Die zusammengelegten HttpMessageHandler-Instanzen resultieren in der Freigabe von CookieContainer-Objekten. Die unerwartete Freigabe von CookieContainer-Objekten führt oft zu fehlerhaftem Code. Ziehen Sie für Apps, die cookies erfordern, folgende Vorgehensweisen in Betracht:

  • Deaktivieren der automatischen cookieverarbeitung
  • Vermeiden der Verwendung von IHttpClientFactory

Rufen Sie ConfigurePrimaryHttpMessageHandler auf, um die automatische cookieverarbeitung zu deaktivieren:

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Protokollierung

Über IHttpClientFactory erstellte Clients zeichnen Protokollmeldungen für alle Anforderungen auf. Aktivieren Sie die entsprechende Informationsebene in der Protokollierungskonfiguration, um die Standardprotokollmeldungen anzuzeigen. Zusätzliche Protokollierung, z.B. das Protokollieren von Anforderungsheadern, wird nur auf der Ablaufverfolgungsebene enthalten.

Die Protokollierungskategorie für jeden Client enthält den Namen des Clients. Ein Client namens MyNamedClient protokolliert z. B. Nachrichten mit der Kategorie „System.Net.Http.HttpClient.MyNamedClient.LogicalHandler“. Meldungen mit dem Suffix LogicalHandler treten außerhalb der Anforderungshandlerpipeline auf. In der Anforderung werden Meldungen protokolliert, bevor andere Handler in der Pipeline sie verarbeitet haben. In der Antwort werden Meldungen protokolliert, nachdem andere Handler in der Pipeline die Antwort empfangen haben.

Die Protokollierung tritt ebenfalls innerhalb der Anforderungshandlerpipeline auf. Im Beispiel von MyNamedClient werden diese Nachrichten mit der Protokollkategorie „System.Net.Http.HttpClient.MyNamedClient.ClientHandler“ protokolliert. Für die Anforderung erfolgt dies, nachdem alle anderen Handler ausgeführt wurden und bevor die Anforderung gesendet wird. Diese Protokollierung enthält den Zustand der Antwort in der Antwort, bevor sie an die Handlerpipeline zurückgegeben wird.

Das Aktivieren der Protokollierung außerhalb und innerhalb der Pipeline ermöglicht die Überprüfung der Änderungen, die durch andere Handler in der Pipeline erfolgt sind. Dies kann die Änderungen an Anforderungsheadern oder am Statuscode der Antwort enthalten.

Das Einschließen des Namens des Clients in der Protokollkategorie aktiviert die Protokollfilterung für spezifische benannte Clients.

Konfigurieren von 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 verwendet werden. Der Delegat wird verwendet, um den primären HttpMessageHandler zu erstellen und konfigurieren, der von dem Client verwendet wird:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

Verwenden von IHttpClientFactory in einer Konsolen-App

Fügen Sie in einer Konsolen-App dem Projekt die folgenden Paketverweise hinzu:

Im folgenden Beispiel:

  • IHttpClientFactory ist im Dienstcontainer des generischen Hosts registriert.
  • MyService erstellt eine Clientfactoryinstanz aus dem-Dienst, der zum Erstellen eines HttpClientverwendet wird. HttpClient wird zum Abrufen einer Webseite verwendet.
  • Main erstellt einen Bereich, um die GetPage-Methode des Diensts auszuführen und die ersten 500 Zeichen des Webseiteninhalts in die Konsole zu schreiben.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

Middleware für Headerweitergabe

Für die Headerweitergabe wird eine ASP.NET Core-Middleware verwendet, um HTTP-Header von eingehenden Anforderungen an ausgehende HTTP-Clientanforderungen weiterzugeben. So verwenden Sie die Headerweitergabe:

  • Erstellen Sie einen Verweis auf das Microsoft.AspNetCore.HeaderPropagation-Paket.

  • Konfigurieren Sie in Startup die Middleware und HttpClient:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseRouting();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  • Der Client schließt die konfigurierten Header für ausgehende Anforderungen ein:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Zusätzliche Ressourcen

Von Kirk Larkin, Steve Gordon, Glenn Condron und Ryan Nowak.

IHttpClientFactory kann registriert und zum Konfigurieren und Erstellen von HttpClient-Instanzen in einer App verwendet werden. IHttpClientFactory bietet die folgenden Vorteile:

  • Ein zentraler Ort für das Benennen und Konfigurieren logischer HttpClient-Instanzen wird damit geboten. Beispielsweise kann ein Client namens github registriert und für den Zugriff auf GitHub konfiguriert werden. Ein Standardclient kann für den allgemeinen Zugriff registriert werden.
  • Das Konzept der ausgehenden Middleware wird über delegierende Handler in HttpClient in Code umgesetzt. Außerdem werden Erweiterungen für auf Polly basierende Middleware bereitgestellt, um die delegierenden Handler in HttpClient zu nutzen.
  • Das Pooling und die Lebensdauer von zugrunde liegenden HttpClientMessageHandler-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.

Zeigen Sie Beispielcode an, oder laden Sie diesen herunter (Vorgehensweise zum Herunterladen).

Der Beispielcode in dieser Version des Themas verwendet System.Text.Json, um JSON-Inhalte zu deserialisieren, die in HTTP-Antworten zurückgegeben wurden. Verwenden Sie bei Beispielen, die Json.NET und ReadAsAsync<T> verwenden, die Versionsauswahl, um eine 2.x-Version dieses Themas auszuwählen.

Verbrauchsmuster

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

Der beste Ansatz richtet sich nach den Anforderungen der App.

Grundlegende Verwendung

IHttpClientFactory kann durch Aufrufen von AddHttpClient registriert werden:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

Eine IHttpClientFactory-Schnittstelle kann mithilfe der Dependency Injection (DI) angefordert werden. Im folgenden Code wird IHttpClientFactory verwendet, um eine HttpClient-Instanz zu erstellen:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

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 HttpClients verfügen über unterschiedliche Konfigurationen.

Die Konfiguration eines benannten HttpClient kann während der Registrierung in Startup.ConfigureServices angegeben werden:

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

Im vorangehenden Code wird der Client mit Folgendem konfiguriert:

  • der Basisadresse https://api.github.com/
  • zwei Header, die für die Arbeit mit der GitHub-API erforderlich sind

CreateClient

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:

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

Im vorangehenden Code muss die 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 Hilfe durch IntelliSense und Compiler beim Verarbeiten 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:

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubIssue>>(responseStream);
    }
}

Wenn Sie möchten, dass Codekommentare in anderen Sprachen als Englisch angezeigt werden, informieren Sie uns in diesem GitHub-Issue.

Für den Code oben gilt:

  • Die Konfiguration wird in den typisierten Client verschoben.
  • Das HttpClient-Objekt wird als öffentliche Eigenschaft zur Verfügung gestellt.

Es können API-spezifische Methoden erstellt werden, die die HttpClient-Funktionalität verfügbar machen. Die GetAspNetDocsIssues-Methode kapselt z. B. Code zum Abrufen offener Probleme.

Der folgende Code ruft AddHttpClient in Startup.ConfigureServices auf, um eine typisierte Clientklasse zu registrieren:

services.AddHttpClient<GitHubService>();

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

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

Der typisierte Client kann direkt eingefügt und verwendet werden:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

Die Konfiguration kann für einen typisierten Client während der Registrierung in Startup.ConfigureServices angegeben werden, anstatt im Konstruktor des typisierten Clients:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

HttpClient kann in einem typisierten Client gekapselt werden. Anstatt ihn als eine Eigenschaft zur Verfügung zu stellen, definieren Sie eine Methode, die die HttpClient-Instanz intern aufruft:

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

Im vorangehenden Code wird HttpClient in einem privaten Feld gespeichert. Der Zugriff auf HttpClient erfolgt durch die öffentliche GetRepos-Methode.

Generierte Clients

IHttpClientFactory kann in Verbindung mit Bibliotheken von Drittanbietern verwendet werden, z. B. Refit. Refit ist eine REST-Bibliothek für .NET. Sie konvertiert REST-APIs in Live-Schnittstellen. Eine Implementierung der Schnittstelle wird dynamisch von RestService generiert. HttpClient wird für die externen HTTP-Aufrufe verwendet.

Eine Schnittstelle und eine Antwort werden definiert, um die externe API und die Antwort darzustellen:

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

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

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

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

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

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.

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

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

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

  • Sie serialisiert den TodoItem-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 SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

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

Das folgende Beispiel zeigt eine HTTP-DELETE-Anforderung.

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    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.

Middleware für ausgehende Anforderungen

HttpClient enthält das Konzept, Handler zu delegieren, die für ausgehende HTTP-Anforderungen miteinander verknüpft werden können. IHttpClientFactory:

  • Erleichtert das Definieren der Handler, die für jeden benannten Client angewendet werden
  • Unterstützt die Registrierung und Verkettung von mehreren Handlern, um eine Pipeline für die Middleware für ausgehende Anforderungen zu erstellen. Jeder dieser Handler kann vor und nach der ausgehenden Anforderung Aufgaben ausführen. Dieses Muster:
    • ähnelt der eingehenden Pipeline für Middleware in ASP.NET Core
    • bietet einen Mechanismus zum Verwalten von übergreifenden Belangen bezüglich HTTP-Anforderungen, z. B.:
      • Zwischenspeicherung
      • Fehlerbehandlung
      • Serialisierung
      • Protokollierung

So erstellen Sie einen delegierenden Handler:

  • Leiten Sie von DelegatingHandler ab.
  • Überschreiben Sie SendAsync. Führen Sie Code aus, bevor die Anforderung an den nächsten Handler in der Pipeline übergeben wird:
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Der vorangehende Code überprüft, ob die Anforderung einen X-API-KEY-Header enthält. Wenn X-API-KEY fehlt, wird BadRequest zurückgegeben.

Für eine HttpClient-Klasse mit Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler kann mehr als ein Handler zur Konfiguration hinzugefügt werden:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

Im vorangehenden Code ist ValidateHeaderHandler mit DI registriert. Nach der Registrierung kann AddHttpMessageHandler aufgerufen werden, was den Typ für den Handler übergibt.

Mehrere Handler können in der Reihenfolge registriert werden, in der sie ausgeführt werden sollen. Jeder Handler umschließt den nächsten Handler, bis der endgültige HttpClientHandler die Anforderung ausführt:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Verwenden von DI in Middleware für ausgehende Anforderungen

Wenn IHttpClientFactory einen neuen delegierenden Handler erstellt, wird DI verwendet, um die Konstruktorparameter des Handlers zu erfüllen. IHttpClientFactory erstellt einen separaten DI-Bereich für jeden Handler. Dies kann zu überraschendem Verhalten führen, wenn ein Handler einen bereichsbezogenen Dienst nutzt.

Sehen Sie sich beispielsweise die folgende Schnittstelle und ihre Implementierung an, die eine Aufgabe als Vorgang mit einem Bezeichner OperationId darstellt:

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Wie der Name bereits vermuten lässt, wird IOperationScoped bei DI registriert, wobei eine bereichsbezogene Lebensdauer verwendet wird:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

Der folgende delegierende Handler verwendet IOperationScoped, um den X-OPERATION-ID-Header für die ausgehende Anforderung festzulegen:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

Navigieren Sie im HttpRequestsSample-Download] zu /Operation, und aktualisieren Sie die Seite. Der Wert für den Anforderungsbereich ändert sich für jede Anforderung, aber der Wert des Handlerbereichs ändert sich nur alle 5 Sekunden.

Handler können von Diensten eines beliebigen Bereichs abhängen. Dienste, von denen Handler abhängig sind, werden verworfen, wenn der Handler verworfen wird.

Verwenden Sie einen der folgenden Ansätze, um den anforderungsspezifischen Zustand mit Meldungshandlern zu teilen:

Verwenden von Polly-basierten Handlern

IHttpClientFactory ist mit der Drittanbieterbibliothek Polly integriert. Polly ist eine umfassende Bibliothek für die Behandlung von beständigen und vorübergehenden Fehlern für .NET. Entwicklern wird damit ermöglicht, Richtlinien wie Wiederholungsrichtlinien, Trennschalterrichtlinien, Timeout-Richtlinien, Bulkhead Isolation-Richtlinien und Ausweichrichtlinien in einer flüssigen und threadsicheren Art und Weise auszudrücken.

Erweiterungsmethoden werden bereitgestellt, um die Verwendung von Polly-Richtlinien mit konfigurierten HttpClient-Instanzen zu aktivieren. Die Polly-Erweiterungen unterstützen das Hinzufügen von Polly-basierten Handlern zu Clients. Polly erfordert das Microsoft.Extensions.Http.Polly-NuGet-Paket.

Behandeln von vorrübergehenden Fehlern

Fehler treten normalerweise auf, wenn externe HTTP-Aufrufe vorübergehend sind. AddTransientHttpErrorPolicy ermöglicht die Definition einer Richtlinie, um vorübergehende Fehler zu behandeln. Mit AddTransientHttpErrorPolicy konfigurierte Richtlinien behandeln die folgenden Antworten:

AddTransientHttpErrorPolicy ermöglicht den Zugriff auf ein PolicyBuilder-Objekt, das für die Fehlerbehandlung konfiguriert wurde und mögliche vorübergehende Fehler darstellt:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

Im vorangehenden Code wird eine WaitAndRetryAsync-Richtlinie definiert. Anforderungsfehler werden bis zu dreimal mit einer Verzögerung von 600 ms wiederholt.

Dynamisches Auswählen von Richtlinien

Erweiterungsmethoden werden zum Hinzufügen von Polly-basierten Handlern bereitgestellt, z. B. AddPolicyHandler. Der folgende AddPolicyHandler-Überladung prüft die Anforderung, um zu entscheiden, welche Richtlinie angewendet werden soll:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

Im vorangehenden Code wird ein 10 Sekunden langer Timeout angewendet, wenn die ausgehende Anforderung eine HTTP GET-Anforderung ist. Für alle anderen HTTP-Methoden wird ein 30 Sekunden langer Timeout verwendet.

Hinzufügen mehrerer Polly-Handler

Es ist üblich, Polly-Richtlinien zu schachteln:

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

Im vorherigen Beispiel:

  • Zwei Handler werden hinzugefügt.
  • Der erste Handler verwendet AddTransientHttpErrorPolicy, um eine Wiederholungsrichtlinie hinzuzufügen. Fehlgeschlagene Anforderungen werden bis zu drei Mal wiederholt.
  • Der zweite AddTransientHttpErrorPolicy-Aufruf fügt eine Trennschalterrichtlinie hinzu. Weitere externe Anforderungen werden 30 Sekunden lang blockiert, wenn fünf Fehlversuche hintereinander stattfinden. Trennschalterrichtlinien sind zustandsbehaftet. Alle Aufrufe über diesen Client teilen den gleichen Schalterzustand.

Hinzufügen von Richtlinien aus der Polly-Registrierung

Eine Methode zum Verwalten regelmäßig genutzter Richtlinien ist, Sie einmal zu definieren und mit PolicyRegistry zu registrieren.

Im folgenden Code wird Folgendes ausgeführt:

  • Die Richtlinien „regular“ und „long“ werden hinzugefügt.
  • AddPolicyHandlerFromRegistry fügt die Richtlinien „regular“ und „long“ aus der Registrierung hinzu.
public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

Weitere Informationen zu IHttpClientFactory und Polly-Integrationen finden Sie im Polly-Wiki.

HttpClient und die Verwaltung der Lebensdauer

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

IHttpClientFactory legt die HttpMessageHandler-Instanzen zusammen, die von der Factory zum Reduzieren des Ressourcenverbrauchs erstellt wurden. Eine HttpMessageHandler-Instanz kann aus dem Pool wiederverwendet werden, wenn eine neue HttpClient-Instanz erstellt wird und deren Lebensdauer noch nicht abgelaufen ist.

Das Zusammenlegen von Handlern ist ein wünschenswerter Vorgang, da jeder Handler in der Regel seine zugrunde liegenden HTTP-Verbindungen selbst verwaltet. Wenn mehr Handler als nötig erstellt werden, können Verzögerungen bei Verbindungen entstehen. Einige Handler halten Verbindungen auch unbegrenzt offen, was verhindert, dass der Handler auf DNS-Änderungen (Domain Name System) reagiert.

Die Standardlebensdauer von Handlern beträgt zwei Minuten. Der Standardwert kann für jeden benannten Client überschrieben werden:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

HttpClient-Instanzen können im Allgemeinen als .NET-Objekte behandelt werden, die nicht verworfen werden müssen. Beim Verwerfen werden ausgehende Anforderungen abgebrochen, und es wird sichergestellt, dass die angegebene HttpClient-Instanz nach dem Aufruf von Dispose nicht mehr verwendet werden kann. IHttpClientFactory verfolgt von HttpClient-Instanzen verwendete Ressourcen nach und verwirft sie.

Das Beibehalten einer einzelnen HttpClient-Instanz für einen langen Zeitraum ist ein allgemeines Muster, das vor der Einführung von IHttpClientFactory verwendet wurde. Dieses Muster wird nach der Migration zu IHttpClientFactory überflüssig.

Alternativen zu IHttpClientFactory

Mit der Verwendung von IHttpClientFactory in einer DI-fähigen App wird Folgendes vermieden:

  • Probleme mit der Ressourcenauslastung durch Zusammenlegen von HttpMessageHandler-Instanzen
  • Probleme durch veraltetes DNS durch regelmäßigen Wechsel von HttpMessageHandler-Instanzen

Es gibt alternative Möglichkeiten zum Lösen des vorangehenden Problems mithilfe einer langlebigen SocketsHttpHandler-Instanz:

  • Erstellen Sie eine SocketsHttpHandler-Instanz, wenn die App gestartet wird, und verwenden Sie diese für die Lebensdauer der App.
  • Konfigurieren Sie einen geeigneten Wert für PooledConnectionLifetime basierend auf den DNS-Aktualisierungszeiten.
  • Erstellen Sie bei Bedarf HttpClient-Instanzen mithilfe von new HttpClient(handler, disposeHandler: false).

Diese Ansätze lösen die Ressourcenverwaltungsprobleme, die IHttpClientFactory auf ähnliche Weise löst.

  • SocketsHttpHandler gibt Verbindungen für HttpClient-Instanzen frei. Durch diese Freigabe wird die Erschöpfung von Sockets vermieden.
  • SocketsHttpHandler wechselt die Verbindungen anhand von PooledConnectionLifetime, um Probleme durch veraltetes DNS zu umgehen.

Cookie

Die zusammengelegten HttpMessageHandler-Instanzen resultieren in der Freigabe von CookieContainer-Objekten. Die unerwartete Freigabe von CookieContainer-Objekten führt oft zu fehlerhaftem Code. Ziehen Sie für Apps, die cookies erfordern, folgende Vorgehensweisen in Betracht:

  • Deaktivieren der automatischen cookieverarbeitung
  • Vermeiden der Verwendung von IHttpClientFactory

Rufen Sie ConfigurePrimaryHttpMessageHandler auf, um die automatische cookieverarbeitung zu deaktivieren:

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Protokollierung

Über IHttpClientFactory erstellte Clients zeichnen Protokollmeldungen für alle Anforderungen auf. Aktivieren Sie die entsprechende Informationsebene in der Protokollierungskonfiguration, um die Standardprotokollmeldungen anzuzeigen. Zusätzliche Protokollierung, z.B. das Protokollieren von Anforderungsheadern, wird nur auf der Ablaufverfolgungsebene enthalten.

Die Protokollierungskategorie für jeden Client enthält den Namen des Clients. Ein Client namens MyNamedClient protokolliert z. B. Nachrichten mit der Kategorie „System.Net.Http.HttpClient.MyNamedClient.LogicalHandler“. Meldungen mit dem Suffix LogicalHandler treten außerhalb der Anforderungshandlerpipeline auf. In der Anforderung werden Meldungen protokolliert, bevor andere Handler in der Pipeline sie verarbeitet haben. In der Antwort werden Meldungen protokolliert, nachdem andere Handler in der Pipeline die Antwort empfangen haben.

Die Protokollierung tritt ebenfalls innerhalb der Anforderungshandlerpipeline auf. Im Beispiel von MyNamedClient werden diese Nachrichten mit der Protokollkategorie „System.Net.Http.HttpClient.MyNamedClient.ClientHandler“ protokolliert. Für die Anforderung erfolgt dies, nachdem alle anderen Handler ausgeführt wurden und bevor die Anforderung gesendet wird. Diese Protokollierung enthält den Zustand der Antwort in der Antwort, bevor sie an die Handlerpipeline zurückgegeben wird.

Das Aktivieren der Protokollierung außerhalb und innerhalb der Pipeline ermöglicht die Überprüfung der Änderungen, die durch andere Handler in der Pipeline erfolgt sind. Dies kann die Änderungen an Anforderungsheadern oder am Statuscode der Antwort enthalten.

Das Einschließen des Namens des Clients in der Protokollkategorie aktiviert die Protokollfilterung für spezifische benannte Clients.

Konfigurieren von 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 verwendet werden. Der Delegat wird verwendet, um den primären HttpMessageHandler zu erstellen und konfigurieren, der von dem Client verwendet wird:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

Verwenden von IHttpClientFactory in einer Konsolen-App

Fügen Sie in einer Konsolen-App dem Projekt die folgenden Paketverweise hinzu:

Im folgenden Beispiel:

  • IHttpClientFactory ist im Dienstcontainer des generischen Hosts registriert.
  • MyService erstellt eine Clientfactoryinstanz aus dem-Dienst, der zum Erstellen eines HttpClientverwendet wird. HttpClient wird zum Abrufen einer Webseite verwendet.
  • Main erstellt einen Bereich, um die GetPage-Methode des Diensts auszuführen und die ersten 500 Zeichen des Webseiteninhalts in die Konsole zu schreiben.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

Middleware für Headerweitergabe

Für die Headerweitergabe wird eine ASP.NET Core-Middleware verwendet, um HTTP-Header von eingehenden Anforderungen an ausgehende HTTP-Clientanforderungen weiterzugeben. So verwenden Sie die Headerweitergabe:

  • Erstellen Sie einen Verweis auf das Microsoft.AspNetCore.HeaderPropagation-Paket.

  • Konfigurieren Sie in Startup die Middleware und HttpClient:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseRouting();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  • Der Client schließt die konfigurierten Header für ausgehende Anforderungen ein:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Zusätzliche Ressourcen

Von Glenn Condron, Ryan Nowak, und Steve Gordon

IHttpClientFactory kann registriert und zum Konfigurieren und Erstellen von HttpClient-Instanzen in einer App verwendet werden. Dies hat folgende Vorteile:

  • Ein zentraler Ort für das Benennen und Konfigurieren logischer HttpClient-Instanzen wird damit geboten. Ein GitHub-Client kann beispielsweise registriert werden und so konfiguriert werden, um auf GitHub zuzugreifen. Ein Standard-Client kann für andere Zwecke registriert werden.
  • Das Konzept der ausgehenden Middleware wird über delegierende Handler in HttpClient in Code umgesetzt. Außerdem werden Erweiterungen für auf Polly basierende Middleware bereitgestellt, die dies nutzen.
  • Das Pooling und die Lebensdauer von zugrunde liegenden HttpClientMessageHandler-Instanzen werden verwaltet, um gängige DNS-Probleme zu vermeiden, die bei der manuellen Verwaltung der HttpClient-Lebensdauer auftreten.
  • Eine konfigurierbare Protokollierungsfunktion wird (über ILogger) für alle Anforderungen hinzugefügt, die über Clients gesendet werden, die von der Factory erstellt wurden.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

Voraussetzungen

Für Projekte mit der Zielplattform .NET Framework muss das NuGet-Paket Microsoft.Extensions.Http installiert werden. Projekte für .NET Core, die auf das Metapaket Microsoft.AspNetCore.App verweisen, enthalten bereits das Microsoft.Extensions.Http-Paket.

Verbrauchsmuster

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

Keine dieser Möglichkeiten ist einer anderen überlegen. Der beste Ansatz richtet sich nach den Einschränkungen der App.

Grundlegende Verwendung

IHttpClientFactory kann durch Aufrufen der Erweiterungsmethode AddHttpClient auf IServiceCollection in der Methode Startup.ConfigureServices registriert werden.

services.AddHttpClient();

Nach der Registrierung kann Code IHttpClientFactory überall akzeptieren, wo Dienste mithilfe der Dependency Injection (DI) eingefügt werden können. IHttpClientFactory kann zum Erstellen einer HttpClient-Instanz verwendet werden:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            Branches = await response.Content
                .ReadAsAsync<IEnumerable<GitHubBranch>>();
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }                               
    }
}

IHttpClientFactory auf diese Weise zu verwenden, ist eine gute Möglichkeit zum Umgestalten einer vorhandenen App. Dies hat keine Auswirkung auf die Verwendung von HttpClient. An Stellen, in denen HttpClient-Instanzen derzeit erstellt werden, können Sie diese Ereignisse mit einem Aufruf von CreateClient ersetzen.

Benannte Clients

Wenn eine App mehrere verschiedene Verwendungen von HttpClient mit jeweils unterschiedlicher Konfiguration erfordert, ist die Verwendung von benannten Clients eine Option. Die Konfiguration einer benannten HttpClient kann während der Registrierung in Startup.ConfigureServices angegeben werden.

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

Im oben stehenden Code wird AddHttpClient aufgerufen, und dabei wird der Name github angegeben. Auf diesen Client wird eine Standardkonfiguration angewendet, d. h. die Basisadresse und zwei Header, die für die Arbeit mit der GitHub-API erforderlich sind.

Wenn CreateClient aufgerufen wird, wird jedes Mal eine neue Instanz von HttpClient erstellt und die Konfigurationsaktion aufgerufen.

Zum Verarbeiten eines benannten Clients kann ein Zeichenfolgenparameter an CreateClient übergeben werden. Geben Sie den Namen des zu erstellenden Clients an:

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            PullRequests = await response.Content
                .ReadAsAsync<IEnumerable<GitHubPullRequest>>();
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

Im vorangehenden Code muss die Anforderung keinen Hostnamen angeben. Sie 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 Hilfe durch IntelliSense und Compiler beim Verarbeiten von Clients.
  • Stellen einen einzelnen Ort zum Konfigurieren und Interagieren mit einem bestimmten HttpClient bereit. Zum Beispiel kann ein einzelner typisierter Client für einen einzelnen Back-End-Endpunkt verwendet werden, der jegliche Logik kapselt, die mit diesem Endpunkt interagiert.
  • Funktionieren mit DI und können überall in Ihrer App eingefügt werden, wo sie benötigt werden.

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

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept", 
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent", 
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<GitHubIssue>>();

        return result;
    }
}

Im vorangehenden Code wird die Konfiguration in den typisierten Client verschoben. Das HttpClient-Objekt wird als öffentliche Eigenschaft zur Verfügung gestellt. Es ist möglich, API-spezifische Methoden zu definieren, die die HttpClient-Funktionalität zur Verfügung stellen. Die Methode GetAspNetDocsIssues kapselt den Code, der für die Abfrage und Analyse des neuesten offenen Problems aus dem GitHub-Repository erforderlich ist.

Zum Registrieren eines typisierten Clients kann die generische Erweiterungsmethode AddHttpClient innerhalb von Startup.ConfigureServices verwendet werden, die die typisierte Klasse angeben:

services.AddHttpClient<GitHubService>();

Der typisierte Client wird mit DI als „vorübergehend“ registriert. Der typisierte Client kann direkt eingefügt und verwendet werden:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

Die Konfiguration kann für einen typisierten Client während der Registrierung in Startup.ConfigureServices angegeben werden, anstatt im Konstruktor des typisierten Clients, falls dies bevorzugt wird:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

Es ist möglich, HttpClient vollständig in einem typisierten Client zu kapseln. Anstatt ihn als eine Eigenschaft zur Verfügung zu stellen, können öffentliche Methoden bereitgestellt werden, die die HttpClient-Instanz intern aufrufen.

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<string>>();

        return result;
    }
}

Im vorangehenden Code wird HttpClient als ein privates Feld gespeichert. Alle Zugriffe, die externe Aufrufe durchführen, durchlaufen die Methode GetRepos.

Generierte Clients

IHttpClientFactory kann in Verbindung mit anderen Bibliotheken von Drittanbietern verwendet werden, z.B. Refit. Refit ist eine REST-Bibliothek für .NET. Sie konvertiert REST-APIs in Live-Schnittstellen. Eine Implementierung der Schnittstelle wird dynamisch von RestService generiert. HttpClient wird für die externen HTTP-Aufrufe verwendet.

Eine Schnittstelle und eine Antwort werden definiert, um die externe API und die Antwort darzustellen:

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

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

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddMvc();
}

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

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

Middleware für ausgehende Anforderungen

HttpClient enthält bereits das Konzept, Handler zu delegieren, die für ausgehende HTTP-Anforderungen miteinander verknüpft werden können. IHttpClientFactory erleichtert das Definieren der Handler, die für jeden benannten Client angewendet werden. Die Registrierung und Verkettung von mehreren Handlern wird unterstützt, um eine Pipeline für die Middleware für ausgehende Anforderungen zu erstellen. Jeder dieser Handler kann vor und nach der ausgehenden Anforderung Aufgaben ausführen. Dieses Muster ähnelt der eingehenden Pipeline für Middleware in ASP.NET Core. Das Muster bietet einen Mechanismus zum Verwalten von übergreifenden Belangen bezüglich HTTP-Anforderungen, einschließlich der Zwischenspeicherung, Fehlerbehandlung, Serialisierung und Protokollierung.

Definieren Sie zum Erstellen eines Handlers eine von DelegatingHandler abgeleitete Klasse. Überschreiben Sie die Methode SendAsync, um Code auszuführen, bevor die Anforderung an den nächsten Handler in der Pipeline übergeben wird:

public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

Der vorangehende Code definiert einen einfachen Handler. Er überprüft, ob ein X-API-KEY-Header in die Anforderung eingefügt wurde. Wenn der Header fehlt, kann er den HTTP-Aufruf vermeiden und eine geeignete Antwort zurückgeben.

Während der Registrierung kann mindestens ein Handler der Konfiguration eines HttpClient hinzugefügt werden. Dieser Vorgang wird über die Erweiterungsmethoden von IHttpClientBuilder ermöglicht.

services.AddTransient<ValidateHeaderHandler>();

services.AddHttpClient("externalservice", c =>
{
    // Assume this is an "external" service which requires an API KEY
    c.BaseAddress = new Uri("https://localhost:5000/");
})
.AddHttpMessageHandler<ValidateHeaderHandler>();

Im vorangehenden Code ist ValidateHeaderHandler mit DI registriert. Der Handler muss in DI als vorübergehender Dienst registriert sein, niemals bereichsbezogen. Wenn der Handler als bereichsbezogener Dienst registriert ist und alle Dienste, von denen der Handler abhängig ist, gelöscht werden können:

  • Die Dienste des Handlers können verworfen werden, bevor der Handler den Gültigkeitsbereich verlässt.
  • Der verworfenen Handlerdienst bewirken, dass der Handler fehlschlägt.

Nach der Registrierung kann AddHttpMessageHandler aufgerufen werden, wobei der Typ für den Handler übergeben wird.

Mehrere Handler können in der Reihenfolge registriert werden, in der sie ausgeführt werden sollen. Jeder Handler umschließt den nächsten Handler, bis der endgültige HttpClientHandler die Anforderung ausführt:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Verwenden Sie einen der folgenden Ansätze, um den anforderungsspezifischen Zustand mit Meldungshandlern zu teilen:

  • Übergeben Sie Daten mit HttpRequestMessage.Properties an den Handler.
  • Greifen Sie mit IHttpContextAccessor auf die aktuelle Anforderung zu.
  • Erstellen Sie ein benutzerdefiniertes AsyncLocal-Speicherobjekt, um die Daten zu übergeben.

Verwenden von Polly-basierten Handlern

IHttpClientFactory integriert mit der beliebten Drittanbieter-Bibliothek namens Polly. Polly ist eine umfassende Bibliothek für die Behandlung von beständigen und vorübergehenden Fehlern für .NET. Entwicklern wird damit ermöglicht, Richtlinien wie Wiederholungsrichtlinien, Trennschalterrichtlinien, Timeout-Richtlinien, Bulkhead Isolation-Richtlinien und Ausweichrichtlinien in einer flüssigen und threadsicheren Art und Weise auszudrücken.

Erweiterungsmethoden werden bereitgestellt, um die Verwendung von Polly-Richtlinien mit konfigurierten HttpClient-Instanzen zu aktivieren. Die Polly-Erweiterungen:

  • Unterstützen das Hinzufügen von Polly-basierten Handlern zu Clients.
  • Können nach Installation des Microsoft.Extensions.Http.Polly-NuGet-Pakets verwendet werden. Das Paket ist nicht im freigegebenen ASP.NET Core-Framework enthalten.

Behandeln von vorrübergehenden Fehlern

Am häufigsten treten Fehler auf, wenn externe HTTP-Aufrufe vorübergehend sind. Eine praktische Erweiterungsmethode namens AddTransientHttpErrorPolicy ist enthalten. Sie ermöglicht das Definieren von Richtlinien zum Behandeln vorübergehender Fehler. Richtlinien, die mit dieser Erweiterungsmethode konfiguriert wurden, behandeln HttpRequestException, HTTP 5xx-Antworten und HTTP 408-Antworten.

Die AddTransientHttpErrorPolicy-Erweiterung kann in Startup.ConfigureServices verwendet werden. Die Erweiterung ermöglicht den Zugriff auf ein PolicyBuilder-Objekt, das für die Fehlerbehandlung konfiguriert wurde und mögliche vorübergehende Fehler darstellt:

services.AddHttpClient<UnreliableEndpointCallerService>()
    .AddTransientHttpErrorPolicy(p => 
        p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

Im vorangehenden Code wird eine WaitAndRetryAsync-Richtlinie definiert. Anforderungsfehler werden bis zu dreimal mit einer Verzögerung von 600 ms wiederholt.

Dynamisches Auswählen von Richtlinien

Es gibt zusätzliche Erweiterungsmethoden, die zum Hinzufügen von Polly-basierten Handlern verwendet werden können. Eine dieser Erweiterungen ist AddPolicyHandler, die über mehrere Überladungen verfügt. Eine Überladung ermöglicht, dass die Anforderung beim Definieren der zu verwendenden Richtlinie überprüft werden kann:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

Im vorangehenden Code wird ein 10 Sekunden langer Timeout angewendet, wenn die ausgehende Anforderung eine HTTP GET-Anforderung ist. Für alle anderen HTTP-Methoden wird ein 30 Sekunden langer Timeout verwendet.

Hinzufügen mehrerer Polly-Handler

Es ist üblich, Polly-Richtlinien zu schachteln, um erweiterte Funktionen bereitzustellen:

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

Im vorangehenden Beispiel werden zwei Handler hinzugefügt. Der Erste verwendet die AddTransientHttpErrorPolicy-Erweiterung, um eine Wiederholungsrichtlinie hinzuzufügen. Fehlgeschlagene Anforderungen werden bis zu drei Mal wiederholt. Der zweite Aufruf von AddTransientHttpErrorPolicy fügt eine Trennschalterrichtlinie hinzu. Weitere externe Anforderungen werden 30 Sekunden lang blockiert, wenn fünf Fehlversuche hintereinander stattfinden. Trennschalterrichtlinien sind zustandsbehaftet. Alle Aufrufe über diesen Client teilen den gleichen Schalterzustand.

Hinzufügen von Richtlinien aus der Polly-Registrierung

Eine Methode zum Verwalten regelmäßig genutzter Richtlinien ist, Sie einmal zu definieren und mit PolicyRegistry zu registrieren. Eine Erweiterungsmethode wird bereitgestellt, die einem Handler ermöglicht, mithilfe einer Richtlinie aus der Registrierung hinzugefügt zu werden:

var registry = services.AddPolicyRegistry();

registry.Add("regular", timeout);
registry.Add("long", longTimeout);

services.AddHttpClient("regulartimeouthandler")
    .AddPolicyHandlerFromRegistry("regular");

Im oben stehenden Code werden zwei Richtlinien registriert, wenn PolicyRegistry zu ServiceCollection hinzugefügt wird. Damit eine Richtlinie aus der Registrierung verwendet werden kann, wird die Methode AddPolicyHandlerFromRegistry verwendet. Dabei wird der Name der anzuwendenden Richtlinie übergeben.

Weitere Informationen zu IHttpClientFactory und Polly-Integrationen finden Sie im Polly Wiki.

HttpClient und die Verwaltung der Lebensdauer

Bei jedem Aufruf von CreateClient in der IHttpClientFactory wird eine neue Instanz von HttpClient zurückgegeben. Es gibt einen HttpMessageHandler pro benanntem Client. Die Factory verwaltet die Lebensdauer der HttpMessageHandler-Instanzen.

IHttpClientFactory legt die HttpMessageHandler-Instanzen zusammen, die von der Factory zum Reduzieren des Ressourcenverbrauchs erstellt wurden. Eine HttpMessageHandler-Instanz kann aus dem Pool wiederverwendet werden, wenn eine neue HttpClient-Instanz erstellt wird und deren Lebensdauer noch nicht abgelaufen ist.

Das Zusammenlegen von Handlern ist ein wünschenswerter Vorgang, da jeder Handler in der Regel seine zugrunde liegenden HTTP-Verbindungen selbst verwaltet. Wenn mehr Handler als nötig erstellt werden, können Verzögerungen bei Verbindungen entstehen. Einige Handler halten Verbindungen auch unbegrenzt offen, was verhindert, dass der Handler auf DNS-Änderungen reagiert.

Die Standardlebensdauer von Handlern beträgt zwei Minuten. Der Standardwert kann für jeden benannten Client überschrieben werden. Rufen Sie SetHandlerLifetime auf dem bei der Erstellung des Clients zurückgegebenen IHttpClientBuilder auf, um den Wert zu überschreiben:

services.AddHttpClient("extendedhandlerlifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Das Verwerfen des Client ist nicht erforderlich. Beim Verwerfen werden ausgehende Anforderungen abgebrochen, und es wird sichergestellt, dass die angegebene HttpClient-Instanz nach dem Aufruf von Dispose nicht mehr verwendet werden kann. IHttpClientFactory verfolgt von HttpClient-Instanzen verwendete Ressourcen nach und verwirft sie. Die HttpClient-Instanzen können im Allgemeinen als .NET-Objekte behandelt werden, die nicht verworfen werden müssen.

Das Beibehalten einer einzelnen HttpClient-Instanz für einen langen Zeitraum ist ein allgemeines Muster, das vor der Einführung von IHttpClientFactory verwendet wurde. Dieses Muster wird nach der Migration zu IHttpClientFactory überflüssig.

Alternativen zu IHttpClientFactory

Mit der Verwendung von IHttpClientFactory in einer DI-fähigen App wird Folgendes vermieden:

  • Probleme mit der Ressourcenauslastung durch Zusammenlegen von HttpMessageHandler-Instanzen
  • Probleme durch veraltetes DNS durch regelmäßigen Wechsel von HttpMessageHandler-Instanzen

Es gibt alternative Möglichkeiten zum Lösen des vorangehenden Problems mithilfe einer langlebigen SocketsHttpHandler-Instanz:

  • Erstellen Sie eine SocketsHttpHandler-Instanz, wenn die App gestartet wird, und verwenden Sie diese für die Lebensdauer der App.
  • Konfigurieren Sie einen geeigneten Wert für PooledConnectionLifetime basierend auf den DNS-Aktualisierungszeiten.
  • Erstellen Sie bei Bedarf HttpClient-Instanzen mithilfe von new HttpClient(handler, disposeHandler: false).

Diese Ansätze lösen die Ressourcenverwaltungsprobleme, die IHttpClientFactory auf ähnliche Weise löst.

  • SocketsHttpHandler gibt Verbindungen für HttpClient-Instanzen frei. Durch diese Freigabe wird die Erschöpfung von Sockets vermieden.
  • SocketsHttpHandler wechselt die Verbindungen anhand von PooledConnectionLifetime, um Probleme durch veraltetes DNS zu umgehen.

Cookie

Die zusammengelegten HttpMessageHandler-Instanzen resultieren in der Freigabe von CookieContainer-Objekten. Die unerwartete Freigabe von CookieContainer-Objekten führt oft zu fehlerhaftem Code. Ziehen Sie für Apps, die cookies erfordern, folgende Vorgehensweisen in Betracht:

  • Deaktivieren der automatischen cookieverarbeitung
  • Vermeiden der Verwendung von IHttpClientFactory

Rufen Sie ConfigurePrimaryHttpMessageHandler auf, um die automatische cookieverarbeitung zu deaktivieren:

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Protokollierung

Über IHttpClientFactory erstellte Clients zeichnen Protokollmeldungen für alle Anforderungen auf. Aktivieren Sie die entsprechende Informationsebene in Ihrer Protokollierungskonfiguration, um die Standardprotokollmeldungen anzuzeigen. Zusätzliche Protokollierung, z.B. das Protokollieren von Anforderungsheadern, wird nur auf der Ablaufverfolgungsebene enthalten.

Die Protokollierungskategorie für jeden Client enthält den Namen des Clients. Ein Client namens MyNamedClient protokolliert beispielsweise Meldungen mit der Kategorie System.Net.Http.HttpClient.MyNamedClient.LogicalHandler. Meldungen mit dem Suffix LogicalHandler treten außerhalb der Anforderungshandlerpipeline auf. In der Anforderung werden Meldungen protokolliert, bevor andere Handler in der Pipeline sie verarbeitet haben. In der Antwort werden Meldungen protokolliert, nachdem andere Handler in der Pipeline die Antwort empfangen haben.

Die Protokollierung tritt ebenfalls innerhalb der Anforderungshandlerpipeline auf. Im Beispiel MyNamedClient werden diese Meldungen für die Protokollkategorie System.Net.Http.HttpClient.MyNamedClient.ClientHandler protokolliert. Dies findet für die Anforderung statt, nachdem alle anderen Handler ausgeführt wurden und bevor die Anforderung an das Netzwerk gesendet wird. Diese Protokollierung enthält den Zustand der Antwort in der Antwort, bevor sie an die Handlerpipeline zurückgegeben wird.

Das Aktivieren der Protokollierung außerhalb und innerhalb der Pipeline ermöglicht die Überprüfung der Änderungen, die durch andere Handler in der Pipeline erfolgt sind. Dies kann beispielsweise die Änderungen an Anforderungsheadern oder am Statuscode der Antwort enthalten.

Das Einschließen des Namens des Clients in der Protokollkategorie aktiviert bei Bedarf die Protokollfilterung für spezifische benannte Clients.

Konfigurieren von 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 verwendet werden. Der Delegat wird verwendet, um den primären HttpMessageHandler zu erstellen und konfigurieren, der von dem Client verwendet wird:

services.AddHttpClient("configured-inner-handler")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            AllowAutoRedirect = false,
            UseDefaultCredentials = true
        };
    });

Verwenden von IHttpClientFactory in einer Konsolen-App

Fügen Sie in einer Konsolen-App dem Projekt die folgenden Paketverweise hinzu:

Im folgenden Beispiel:

  • IHttpClientFactory ist im Dienstcontainer des generischen Hosts registriert.
  • MyService erstellt eine Clientfactoryinstanz aus dem-Dienst, der zum Erstellen eines HttpClientverwendet wird. HttpClient wird zum Abrufen einer Webseite verwendet.
  • Die Methode GetPage des Diensts wird ausgeführt, um die ersten 500 Zeichen des Webseiteninhalts in die Konsole zu schreiben. Weitere Informationen zum Aufrufen von Diensten über Program.Main finden Sie unter Abhängigkeitsinjektion in ASP.NET Core.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

Middleware für Headerweitergabe

Für die Headerweitergabe wird eine von der Community unterstützte Middleware verwendet, um HTTP-Header von eingehenden Anforderungen an ausgehende HTTP-Clientanforderungen weiterzugeben. So verwenden Sie die Headerweitergabe:

  • Erstellen Sie einen Verweis auf das von der Community unterstützte HeaderPropagation-Paket. ASp.NET Core 3.1 und höher unterstützen Microsoft.AspNetCore.HeaderPropagation.

  • Konfigurieren Sie in Startup die Middleware und HttpClient:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseMvc();
    }
    
  • Der Client schließt die konfigurierten Header für ausgehende Anforderungen ein:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Zusätzliche Ressourcen