Häufige Probleme bei der Verwendung von IHttpClientFactory
In diesem Artikel lernen Sie einige der häufigsten Probleme kennen, die beim IHttpClientFactory
Erstellen von HttpClient
-Instanzen auftreten können.
IHttpClientFactory
ist eine bequeme Möglichkeit, mehrere HttpClient
Konfigurationen im DI-Container einzurichten, die Protokollierung zu konfigurieren, Resilienzstrategien einzurichten und vieles mehr. IHttpClientFactory
kapselt auch die Lebensdauerverwaltung und HttpClient
HttpMessageHandler
Instanzen, um Probleme wie Socketausschöpfung und Verlust von DNS-Änderungen zu verhindern. Eine Übersicht zur Verwendung von IHttpClientFactory
in Ihrer .NET-Anwendung finden Sie unter IHttpClientFactory mit .NET.
Aufgrund einer komplexen Art der IHttpClientFactory
-Integration mit DI können einige Probleme auftreten, die möglicherweise schwer zu erfassen und zu beheben sind. Die in diesem Artikel aufgeführten Szenarien enthalten auch Empfehlungen, die Sie proaktiv anwenden können, um potenzielle Probleme zu vermeiden.
HttpClient
respektiert Scoped
die Lebensdauer nicht
Sie können ein Problem feststellen, wenn Sie auf einen beliebigen bereichsbezogenen Dienst, zum Beispiel HttpContext
bzw. auf einen bereichsbezogenen Cache von HttpMessageHandler
aus zugreifen müssen. Die dort gespeicherten Daten können entweder „verschwinden“ oder umgekehrt, „bestehen bleiben“, obwohl sie das nicht sollen. Dies wird durch den Dependency Injection (DI)-Bereichskonflikt zwischen dem Anwendungskontext und der Handlerinstanz verursacht, und es ist eine bekannte Einschränkung in IHttpClientFactory
.
IHttpClientFactory
erstellt für jede HttpMessageHandler
-Instanz einen separaten DI-Bereich. Diese Handlerbereiche sind von Anwendungskontextbereichen getrennt (z. B. vom ASP.NET Core-Bereich für eingehende Anforderungen oder einem vom Benutzer erstellten manuellen DI-Bereich), sodass sie keine bereichsbezogenen Dienstinstanzen gemeinsam nutzen.
Auswirkungen durch diese Beschränkung:
- Alle Daten, die in einem bereichsbezogenen Dienst „extern“ zwischengespeichert wurden, sind nicht innerhalb des
HttpMessageHandler
-Bereichs verfügbar. - Alle Daten, die innerhalb von
HttpMessageHandler
oder der bereichsbezogenen Abhängigkeiten zwischengespeichert werden, können aus mehreren Anwendungs-DI-Bereichen (z. B. aus unterschiedlichen eingehenden Anforderungen) beobachtet werden, da sie denselben Handler gemeinsam nutzen können.
Beachten Sie die folgenden Empfehlungen, um diese bekannte Einschränkung zu beheben:
❌ Zwischenspeichern Sie KEINE bereichsbezogenen Informationen (z. B. Daten von HttpContext
) innerhalb derHttpMessageHandler
-Instanzen oder deren Abhängigkeiten, um zu vermeiden, dass vertrauliche Informationen verloren gehen.
❌ Verwenden Sie KEINE Cookies, da CookieContainer
gemeinsam mit dem Handler freigegeben wird.
✔️ ERWÄGEN Sie, die Informationen nicht zu speichern oder nur innerhalb der HttpRequestMessage
-Instanz zu übergeben.
Um beliebige Informationen zusammen mit der HttpRequestMessage
zu übergeben, können Sie die HttpRequestMessage.Options-Eigenschaft verwenden.
✔️ ERWÄGEN Sie, die gesamte bereichsbezogene Logik (z. B. Authentifizierung) in einem separaten DelegatingHandler
, der nicht von IHttpClientFactory
erstellt wurde, und verwenden Sie ihn, um den von IHttpClientFactory
erstellten Handler zu verpacken.
Um nur einen HttpMessageHandler
ohne HttpClient
zu erstellen, rufen Sie IHttpMessageHandlerFactory.CreateHandler für einen beliebigen registrierten benannten Client auf. In diesem Fall müssen Sie eine HttpClient
-Instanz selbst mithilfe des kombinierten Handlers erstellen. Sie finden ein vollständig ausgeführtes Beispiel für diese Problemumgehung auf GitHub.
Weitere Informationen finden Sie im Abschnitt Nachrichtenhandlerbereiche in IHttpClientFactory in den IHttpClientFactory
-Richtlinien.
HttpClient
berücksichtigt DNS-Änderungen nicht
Auch wenn IHttpClientFactory
verwendet wird, ist es weiterhin möglich, dass das veraltete DNS-Problem auftritt. Dies kann in der Regel passieren, wenn eine HttpClient
-Instanz in einem Singleton
-Dienst erfasst wird oder im Allgemeinen für einen bestimmten Zeitraum gespeichert wird, der länger als der angegebene HandlerLifetime
ist. HttpClient
wird auch erfasst, wenn der jeweilige typisierte Client von einem Singleton erfasst wird.
❌ Zwischenspeichern Sie KEINE HttpClient
-Instanzen, die von IHttpClientFactory
erstellt wurden, für einen längeren Zeitraum.
❌ Injizieren Sie die Instanzen des typisierten Client NICHT in die Singleton
-Dienste.
✔️ ERWÄGEN Sie, rechtzeitig oder jedes Mal, wenn Sie einen benötigen, einen Client aus IHttpClientFactory
anzufordern. Vom Hersteller erstellte Clients können sicher verworfen werden.
HttpClient
-Instanzen, die von IHttpClientFactory
erstellt werden, sollen kurzlebig sein.
Das Recyceln und erneute Erstellen von
HttpMessageHandler
n nach Ablauf ihrer Lebensdauer ist fürIHttpClientFactory
unerlässlich, um sicherzustellen, dass Handler auf DNS-Änderungen reagieren.HttpClient
ist bei der Erstellung an eine bestimmte Handlerinstanz gebunden, sodass neueHttpClient
-Instanzen rechtzeitig angefordert werden sollten, um sicherzustellen, dass der Client den aktualisierten Handler erhält.Die Entsorgung solcher
HttpClient
-Instanzen, die von der Factory erstellt wurden, führt nicht zu Socketerschöpfung, da ihre Entsorgung keine Entsorgung desHttpMessageHandler
auslöst.IHttpClientFactory
verfolgt Ressourcen nach und entsorgt diese, die zum Erstellen vonHttpClient
-Instanzen verwendet werden, insbesondere dieHttpMessageHandler
-Instanzen, sobald ihre Lebensdauer abläuft und sie nicht mehr von einemHttpClient
verwendet werden.
Typisierte Clients sind auch nur für den kurzfristigen Einsatz gedacht, da eine HttpClient
-Instanz in den Konstruktor eingefügt wird, sodass sie die Lebensdauer des typisierten Clients freigibt.
Weitere Informationen finden Sie unter HttpClient
Lebensdauerverwaltung und Vermeiden von typisierten Clients in Singleton-Diensten in den IHttpClientFactory
-Richtlinien.
HttpClient
verwendet zu viele Sockets
Selbst wenn IHttpClientFactory
verwendet wird, ist es weiterhin möglich, das Problem der Socketerschöpfung mit einem bestimmten Verwendungsszenario zu erreichen. Beschränkt standardmäßig HttpClient
nicht die Anzahl gleichzeitiger Anforderungen. Wenn eine große Anzahl von HTTP/1.1-Anforderungen gleichzeitig gestartet wird, löst jeder von ihnen einen neuen HTTP-Verbindungsversuch aus, da keine freie Verbindung im Pool besteht und kein Grenzwert festgelegt ist.
❌ Starten Sie KEINE große Anzahl von HTTP/1.1-Anforderungen gleichzeitig, ohne die Grenzwerte anzugeben.
✔️ ERWÄGEN Sie die Einstellung HttpClientHandler.MaxConnectionsPerServer (oder SocketsHttpHandler.MaxConnectionsPerServer, wenn Sie sie als primären Handler verwenden), um einen angemessenen Wert zu verwenden. Beachten Sie, dass diese Grenzwerte nur für die spezifische Handlerinstanz gelten.
✔️ ERWÄGEN Sie die Verwendung von HTTP/2, wodurch Multiplexing-Anforderungen über eine einzelne TCP-Verbindung möglich sind.
Der typisierte Client hat den falschen HttpClient
Injiziert.
Es kann verschiedene Situationen geben, in denen ein unerwarteter HttpClient
in einen typisierten Client injiziert werden kann. Meistens wird die Ursache in einer fehlerhaften Konfiguration auftreten, da jede nachfolgende Registrierung eines Diensts die vorherige außer Kraft setzt.
Typisierte Clients verwenden benannte Clients „unter der Haube“: durch das Hinzufügen eines typisierten Clients wird er impliziert registriert und mit einem benannten Client verknüpft. Der Clientname wird auf den Typnamen von TClient
festgelegt, es sei denn er wurde explizit angegeben. Dies wäre der erste aus dem TClient,TImplementation
-Paar, wenn die AddHttpClient<TClient,TImplementation>
-Overloads verwendet werden.
Daher werden durch das Registrieren eines typisierten Clients zwei getrennte Aktionen ausgeführt:
- Registriert einen benannten Client (in einem einfachen Standardfall lautet der Name
typeof(TClient).Name
). - Registriert einen
Transient
-Dienst mithilfe vonTClient
oder des bereitgestelltenTClient,TImplementation
.
Die folgenden beiden Aussagen sind technisch identisch:
services.AddHttpClient<ExampleClient>(c => c.BaseAddress = new Uri("http://example.com"));
// -OR-
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")) // register named client
.AddTypedClient<ExampleClient>(); // link the named client to a typed client
In einem einfachen Fall ähnelt dies dem folgenden Fall:
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")); // register named client
// register plain Transient service and link it to the named client
services.AddTransient<ExampleClient>(s =>
new ExampleClient(
s.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(ExampleClient))));
Sehen Sie sich die folgenden Beispiele an, wie die Verknüpfung zwischen einem typisierten und einem benannten Client unterbrochen werden kann.
Der typisierte Client wird ein zweites Mal registriert
❌ Registrieren Sie den typisierten Client NICHT separat – er wird bereits automatisch durch den AddHttpClient<T>
-Aufruf registriert.
Wenn ein typisierter Client fälschlicherweise ein zweites Mal als einfacher Transient-Dienst registriert wird, überschreibt dies die durch den HttpClientFactory
benannten Client hinzugefügte Registrierung, und die Verknüpfung mit dem benannten Client wird aufgehoben. Es manifestiert sich so, als ob die Konfiguration von HttpClient
verloren geht, da ein nicht konfigurierter HttpClient
stattdessen in den typisierten Client eingefügt wird.
Es kann verwirrend sein, dass anstelle einer Ausnahme ein „falscher“ HttpClient
verwendet wird. Dies geschieht, da der nicht konfigurierte „standardmäßige“ HttpClient
– der Client mit dem Namen Options.DefaultName (string.Empty
) – als einfacher Transient-Dienst registriert ist, um das einfachste HttpClientFactory
-Verwendungsszenario zu aktivieren. Nach die Verknüpfung aufgehoben wird und der typisierte Client zu einem gewöhnlichen Dienst wird, wird dieser „standardmäßige“ HttpClient
natürlich in den entsprechenden Konstruktorparameter eingefügt.
Verschiedene typisierte Clients werden auf einer gemeinsamen Schnittstelle registriert.
Wenn zwei verschiedene typisierte Clients auf einer gemeinsamen Schnittstelle registriert sind, würden beide denselben benannten Client wiederverwenden. Dies kann wie der erste typisierte Client aussehen, der den zweiten benannten Client „falsch“ injiziert hat.
❌ Registrieren Sie NICHT mehrere typisierte Clients auf einer einzigen Schnittstelle, ohne den Namen explizit anzugeben.
✔️ ERWÄGEN Sie, einen benannten Client separat zu registrieren und zu konfigurieren, und verknüpfen Sie ihn dann mit einem oder mehreren typisierten Clients, indem Sie entweder den Namen im AddHttpClient<T>
Aufruf oder durch das Aufrufen AddTypedClient
während der Einrichtung des benannten Clients angeben.
Wenn Sie einen benannten Client mit demselben Namen mehrmals registrieren, werden die Konfigurationsaktionen automatisch an die Liste der vorhandenen Aktionen angehängt. Dieses Verhalten von HttpClientFactory
ist möglicherweise nicht offensichtlich, aber es ist derselbe Ansatz, der von den Optionsmustern und Konfigurations-APIs, wie Configure verwendet wird.
Dies ist hauptsächlich nützlich für erweiterte Handlerkonfigurationen, z. B. das Hinzufügen eines benutzerdefinierten Handlers zu einem benannten Client, der extern definiert wurde, oder das Modell eines primären Handlers für Tests, funktioniert aber auch für die HttpClient
-Instanzkonfiguration. Die drei folgenden Beispiele führen beispielsweise zu einem HttpClient
, der auf die gleiche Weise (sowohl BaseAddress
als auch DefaultRequestHeaders
sind festgelegt) konfiguriert wurde:
// one configuration callback
services.AddHttpClient("example", c =>
{
c.BaseAddress = new Uri("http://example.com");
c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0");
});
// -OR-
// two configuration callbacks
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"))
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));
// -OR-
// two configuration callbacks in separate AddHttpClient calls
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient("example")
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));
Dies ermöglicht das Verknüpfen eines typisierten Clients mit einem bereits definierten benannten Client und auch das Verknüpfen mehrerer typisierter Clients mit einem einzelnen benannten Client. Es ist offensichtlicher, wenn Overloads mit einem name
-Parameter verwendet werden:
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress));
services.AddHttpClient<FooLogger>("LogClient");
services.AddHttpClient<BarLogger>("LogClient");
Dasselbe kann auch durch Aufrufen von AddTypedClient während der Konfiguration des benannten Clients erreicht werden:
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress))
.AddTypedClient<FooLogger>()
.AddTypedClient<BarLogger>();
Wenn Sie jedoch nicht denselben benannten Client wiederverwenden möchten, Sie aber dennoch die Clients auf derselben Schnittstelle registrieren möchten, können Sie dies tun, indem Sie explizit unterschiedliche Namen dafür angeben:
services.AddHttpClient<ITypedClient, ExampleClient>(nameof(ExampleClient),
c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient<ITypedClient, GithubClient>(nameof(GithubClient),
c => c.BaseAddress = new Uri("https://github.com"));