Verwenden von IHttpClientFactory zur Implementierung robuster HTTP-Anforderungen

Tipp

Diese Inhalte sind ein Auszug aus dem eBook „.NET Microservices Architecture for Containerized .NET Applications“, verfügbar unter .NET Docs oder als kostenlos herunterladbare PDF-Datei, die offline gelesen werden kann.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

IHttpClientFactory ist ein Vertrag, der von DefaultHttpClientFactory, einer Factory, die seit .NET Core 2.1 für das Erstellen von HttpClient-Instanzen verfügbar ist, die in Ihren Anwendungen verwendet werden sollen.

Probleme mit den ursprünglichen HttpClient-Klassen von .NET

Die ursprüngliche und bekannte HttpClient-Klasse kann problemlos verwendet werden, allerdings wird sie von vielen Entwicklern in einigen Fällen nicht richtig verwendet.

Obwohl diese Klasse IDisposable implementiert, wird die Deklaration und Instanziierung innerhalb einer using-Anweisung nicht bevorzugt, da beim Verwerfen des HttpClient-Objekts der zugrunde liegende Socket nicht sofort freigegeben wird, was zur Erschöpfung von Sockets führen kann. Weitere Informationen zu diesem Problem finden Sie im Blogbeitrag You're using HttpClient wrong and it is destabilizing your software (Sie verwenden HttpClient falsch und destabilisieren dadurch Ihre Software).

Deshalb sollte HttpClient einmal instanziiert und während der Lebensdauer einer Anwendung wiederverwendet werden. Das Instanziieren einer HttpClient-Klasse für jede Anforderung erschöpft die Anzahl der verfügbaren Sockets und führt zu hoher Auslastung. Dieses Problem führt zu SocketException-Fehlern. Mögliche Ansätze zur Lösung dieses Problems basieren auf der Erstellung des HttpClient-Objekts als Singleton-Objekt oder statisches Objekt. Dies wird in diesem Microsoft-Artikel zur Verwendung von HttpClient erläutert. Das kann eine gute Lösung für kurzlebige Konsolen-Apps o. ä. sein, die mehrmals am Tag ausgeführt werden.

Ein weiteres Problem, auf das Entwickler stoßen, ist die Verwendung einer gemeinsam genutzten Instanz von HttpClient in zeitintensiven Prozessen. In einer Situation, in der der HttpClient als Singleton oder statisches Objekt instanziiert wird, kann er die DNS-Änderungen nicht wie in dieser Ausgabe des GitHub-Repositorys „dotnet/runtime“ beschrieben behandeln.

Es geht jedoch nicht wirklich um HttpClient an sich, sondern um den Standardkonstruktor für HttpClient, da er eine neue konkrete Instanz von HttpMessageHandler erstellt, die die oben erwähnten Probleme mit der Erschöpfung der Sockets und den DNS-Änderungen aufweist.

Um die oben genannten Probleme zu beheben und HttpClient-Instanzen verwaltbar zu machen, bietet .NET Core 2.1 nun zwei Ansätze, von denen einer IHttpClientFactory ist. Es handelt sich um eine Schnittstelle, die zum Konfigurieren und Erstellen von HttpClient-Instanzen in einer App über Dependency Injection (DI, Abhängigkeitsinjektion) verwendet wird. Sie bietet auch Erweiterungen für auf Polly basierende Middleware, um die Vorteile der delegierenden Handler in HttpClient zu nutzen.

Die Alternative besteht darin, SocketsHttpHandler mit konfigurierter PooledConnectionLifetime zu verwenden. Dieser Ansatz gilt für langlebige static-Instanzen oder Singleton-Instanzen von HttpClient. Weitere Informationen zu den verschiedenen Strategien finden Sie unter HttpClient-Richtlinien für .NET.

Polly ist eine Bibliothek zur Behandlung vorübergehender Fehler, mit der Entwickler ihren Anwendungen Resilienz hinzufügen können, indem sie einige vordefinierte Richtlinien auf fließende und threadsichere Weise verwenden.

Vorteile der Verwendung von IHttpClientFactory

Die derzeitige Implementierung von IHttpClientFactory, die auch IHttpMessageHandlerFactory implementiert, bietet die folgenden Vorteile:

  • Bereitstellen eines zentralen Orts für das Benennen und Konfigurieren logischer HttpClient-Objekte. Sie können beispielsweise einen Client (Dienst-Agent) konfigurieren, der für das Zugreifen auf einen bestimmten Microservice vorkonfiguriert ist.
  • Für das Umsetzen des Konzepts der ausgehenden Middleware über delegierende Handler in HttpClient in Code sowie für das Implementieren von Polly-basierter Middleware, um die Polly-Richtlinien für die Resilienz zu nutzen.
  • HttpClient enthält bereits das Konzept, Handler zu delegieren, die für ausgehende HTTP-Anforderungen miteinander verknüpft werden könnten. Sie können HTTP-Clients in der Factory registrieren und einen Polly-Handler verwenden, um Polly-Richtlinien für Wiederholungen, CircuitBreakers usw. verwenden zu können.
  • Für das Verwalten der Lebensdauer von HttpMessageHandler-Meldungshandlern, um die erwähnten Probleme zu vermeiden, die auftreten können, wenn Sie die HttpClient-Lebensdauer selbst verwalten.

Tipp

Die per DI eingefügten HttpClient-Instanzen können sicher verworfen werden, da der zugehörige HttpMessageHandler von der Factory verwaltet wird. Eingefügte HttpClient-Instanzen sind aus DI-Perspektive vorübergehend, während HttpMessageHandler-Instanzen als bereichsbezogen betrachtet werden können. HttpMessageHandler-Instanzen verfügen über eigene DI-Bereiche, die von den Anwendungsbereichen getrennt sind (z. B. eingehende Anforderungsbereiche in ASP.NET). Weitere Informationen finden Sie unter Verwenden von HttpClientFactory in .NET.

Hinweis

Die Implementierung von IHttpClientFactory (DefaultHttpClientFactory) ist eng an die Implementierung der Abhängigkeitsinjektion im NuGet-Paket Microsoft.Extensions.DependencyInjection gebunden. Wenn Sie HttpClient ohne DI oder mit anderen DI-Implementierungen verwenden müssen, sollten Sie einen static- oder Singleton-HttpClient mit eingerichteter PooledConnectionLifetime verwenden. Weitere Informationen finden Sie unter HttpClient-Richtlinien für .NET.

Mehrere Verwendungsmöglichkeiten für IHttpClientFactory

Es gibt mehrere Methoden, um IHttpClientFactory in Ihrer Anwendung zu verwenden:

  • Grundlegende Verwendung
  • Verwenden Sie benannte Clients.
  • Verwenden Sie typisierte Clients.
  • Verwenden Sie generierte Clients.

Aus Gründen der Übersichtlichkeit zeigt diese Anleitung die strukturierteste Art der Verwendung von IHttpClientFactory, also die Verwendung von typisierten Clients (Dienst-Agent-Muster). Allerdings sind alle Optionen dokumentiert und werden zurzeit in diesem Artikel zur Verwendung von IHttpClientFactory aufgeführt.

Hinweis

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

Verwenden von typisierten Clients mit IHttpClientFactory

Was ist ein „typisierter Client“? Es ist nur ein HttpClient, der für eine bestimmte Verwendung vorkonfiguriert ist. Diese Konfiguration kann bestimmte Werte wie den Basisserver, HTTP-Header oder Timeouts enthalten.

Im folgenden Diagramm wird veranschaulicht, wie typisierte Clients mit IHttpClientFactory verwendet werden:

Diagram showing how typed clients are used with IHttpClientFactory.

Abbildung 8-4. Verwenden von IHttpClientFactory mit typisierten Clientklassen.

In der obigen Abbildung verwendet ein (von einem Controller oder Clientcode verwendeter) ClientService einen HttpClient, der von der registrierten IHttpClientFactory erstellt wird. Diese Factory weist einen HttpMessageHandler aus einem Pool dem HttpClient zu. Der HttpClient kann bei Registrierung der IHttpClientFactory im DI-Container mit der Erweiterungsmethode AddHttpClient mit Pollys Richtlinien konfiguriert werden.

Um die obige Struktur zu konfigurieren, fügen Sie IHttpClientFactory in Ihrer Anwendung hinzu, indem Sie das NuGet-Paket Microsoft.Extensions.Http installieren, das die AddHttpClient-Erweiterungsmethode für IServiceCollection beinhaltet. Diese Erweiterungsmethode registriert die interne DefaultHttpClientFactory-Klasse für die Verwendung als Singleton für die Schnittstelle IHttpClientFactory. Dadurch wird eine temporäre Konfiguration für HttpMessageHandlerBuilder definiert. Dieser Meldungshandler (HttpMessageHandler-Objekt), der aus einem Pool abgerufen wurde, wird von dem HttpClient-Objekt verwendet, das von der Factory zurückgegebenen wird.

Im nächsten Codeschnipsel wird veranschaulicht, wie AddHttpClient() verwendet werden kann, um typisierte Clients (Dienst-Agents) zu registrieren, die HttpClient verwenden müssen.

// Program.cs
//Add http client services at ConfigureServices(IServiceCollection services)
builder.Services.AddHttpClient<ICatalogService, CatalogService>();
builder.Services.AddHttpClient<IBasketService, BasketService>();
builder.Services.AddHttpClient<IOrderingService, OrderingService>();

Wenn Sie die Clientdienste wie im vorherigen Codeschnipsel gezeigt registrieren, erstellt DefaultClientFactory für jeden Dienst einen standardmäßigen HttpClient. Der typisierte Client wird mit einem DI-Container als „vorübergehend“ registriert. Im vorhergehenden Code werden CatalogService, BasketService und OrderingService von AddHttpClient() als vorübergehende Dienste registriert, sodass sie direkt eingefügt und genutzt werden können, ohne dass zusätzliche Registrierungen erforderlich sind.

Sie können der Registrierung auch eine instanzspezifische Konfiguration hinzufügen (beispielsweise die Basisadresse konfigurieren und einige Resilienzrichtlinien hinzufügen), wie im Folgenden gezeigt:

builder.Services.AddHttpClient<ICatalogService, CatalogService>(client =>
{
    client.BaseAddress = new Uri(builder.Configuration["BaseUrl"]);
})
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy());

In diesem nächsten Beispiel sehen Sie die Konfiguration einer der oben genannten Richtlinien:

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
        .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}

Weitere Informationen zum Verwenden von Polly finden Sie im nächsten Artikel.

HTTPClient-Lebensdauer

Jedes Mal, wenn Sie ein HttpClient-Objekt von der IHttpClientFactory abrufen, wird eine neue Instanz zurückgegeben. Aber jeder HttpClient verwendet einen HttpMessageHandler, der zu einem Pool gehört und von der IHttpClientFactory wiederverwendet wird, um den Ressourcenverbrauch zu reduzieren, solange die Lebensdauer des HttpMessageHandler nicht abgelaufen ist.

Das Zusammenlegen von Handlern ist wünschenswert, da jeder Handler in der Regel über seine eigenen HTTP-Verbindungen verfügt. Das Erstellen von mehr Handlern als notwendig kann zu Verzögerungen bei der Verbindung führen. Einige Handler halten Verbindungen auch unbegrenzt offen, was verhindert, dass der Handler auf DNS-Änderungen reagiert.

Die HttpMessageHandler-Objekte im Pool haben eine Lebensdauer, die der Zeitspanne entspricht, in der eine HttpMessageHandler-Instanz im Pool wiederverwendet werden kann. Der Standardwert beträgt zwei Minuten, kann jedoch für jeden typisierten Client überschrieben werden. Rufen Sie SetHandlerLifetime() auf dem bei der Erstellung des Clients zurückgegebenen IHttpClientBuilder auf, um den Wert zu überschreiben, wie im folgenden Code gezeigt:

//Set 5 min as the lifetime for the HttpMessageHandler objects in the pool used for the Catalog Typed Client
builder.Services.AddHttpClient<ICatalogService, CatalogService>()
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Für jeden typisierten Client kann ein eigener Wert für die Lebensdauer des Handlers konfiguriert werden. Legen Sie die Lebensdauer auf InfiniteTimeSpan fest, um das Ablaufen des Handlers zu deaktivieren.

Implementieren von typisierten Clientklassen, die das injizierte und konfigurierte HttpClient-Objekt verwenden

Zuvor müssen Sie Ihre typisierten Clientklassen definieren, z. B. die Klassen im Beispielcode („BasketService“, „CatalogService“, „OrderingService“ usw.). Bei einem typisierten Client handelt es sich um eine Klasse, die ein HttpClient-Objekt akzeptiert (das über deren Konstruktor injiziert wird) und dieses verwendet, um HTTP-Remotedienste aufzurufen. Zum Beispiel:

public class CatalogService : ICatalogService
{
    private readonly HttpClient _httpClient;
    private readonly string _remoteServiceBaseUrl;

    public CatalogService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Catalog> GetCatalogItems(int page, int take,
                                               int? brand, int? type)
    {
        var uri = API.Catalog.GetAllCatalogItems(_remoteServiceBaseUrl,
                                                 page, take, brand, type);

        var responseString = await _httpClient.GetStringAsync(uri);

        var catalog = JsonConvert.DeserializeObject<Catalog>(responseString);
        return catalog;
    }
}

Der typisierte Client (CatalogService im Beispiel) wird per DI (Dependency Injection) aktiviert. Das bedeutet, dass dieser alle registrierten Dienste (zusätzlich zu HttpClient) im Konstruktor akzeptieren kann.

Ein typisierter Client ist ein temporäres Objekt. Das bedeutet, dass immer dann eine neue Instanz erstellt wird, wenn eine benötigt wird. Bei jeder Konstruktion wird also eine neue HttpClient-Instanz empfangen. Die HttpMessageHandler-Objekte im Pool sind jedoch die Objekte, die von mehreren HttpClient-Instanzen wiederverwendet werden.

Verwenden von typisierten Clientklassen

Nachdem Sie Ihre typisierten Klassen implementiert haben, können Sie sie registrieren und mit AddHttpClient() konfigurieren. Danach können Sie sie überall dort verwenden, wo Dienste per DI eingefügt werden, z. B. im Razor Pages-Code oder in einem MVC-Web-App-Controller, wie im folgenden Code von eShopOnContainers gezeigt:

namespace Microsoft.eShopOnContainers.WebMVC.Controllers
{
    public class CatalogController : Controller
    {
        private ICatalogService _catalogSvc;

        public CatalogController(ICatalogService catalogSvc) =>
                                                           _catalogSvc = catalogSvc;

        public async Task<IActionResult> Index(int? BrandFilterApplied,
                                               int? TypesFilterApplied,
                                               int? page,
                                               [FromQuery]string errorMsg)
        {
            var itemsPage = 10;
            var catalog = await _catalogSvc.GetCatalogItems(page ?? 0,
                                                            itemsPage,
                                                            BrandFilterApplied,
                                                            TypesFilterApplied);
            //… Additional code
        }

        }
}

Bis zu diesem Punkt hat der obige Codeschnipsel nur das Beispiel zum Ausführen regulärer HTTP-Anforderungen gezeigt. In den folgenden Abschnitten wird jedoch gezeigt, wie alle HTTP-Anforderungen von HttpClient mit resilienten Richtlinien wie Wiederholungen mit exponentiellem Backoff, Trennschaltern, Sicherheitsfeatures mit Authentifizierungstoken oder beliebigen benutzerdefinierten Features kombiniert werden können. Hierfür müssen Sie nur Richtlinien hinzufügen und den registrierten typisierten Clients Handler zuweisen.

Zusätzliche Ressourcen