From e3a400af60bf506048dec6d5611bd665561ec3a9 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Mon, 20 Jan 2025 20:19:02 +0000 Subject: [PATCH 1/9] (WIP) Keyed DI support in HCF --- .../extensions/httpclient-factory-keyed-di.md | 339 ++++++++++++++++++ .../snippets/http/keyedservices/Program.cs | 22 ++ .../http/keyedservices/keyedservices.csproj | 10 + 3 files changed, 371 insertions(+) create mode 100644 docs/core/extensions/httpclient-factory-keyed-di.md create mode 100644 docs/core/extensions/snippets/http/keyedservices/Program.cs create mode 100644 docs/core/extensions/snippets/http/keyedservices/keyedservices.csproj diff --git a/docs/core/extensions/httpclient-factory-keyed-di.md b/docs/core/extensions/httpclient-factory-keyed-di.md new file mode 100644 index 0000000000000..6fa73ad7a1c39 --- /dev/null +++ b/docs/core/extensions/httpclient-factory-keyed-di.md @@ -0,0 +1,339 @@ +--- +title: Keyed DI Support in IHttpClientFactory +description: Learn how to integrate IHttpClientFactory with Keyed Services. +author: CarnaViire +ms.author: knatalia +ms.date: 01/03/2025 +--- + +# Keyed DI Support in `IHttpClientFactory` + +In this article, you'll learn how to integrate `IHttpClientFactory` with Keyed Services. + +[_Keyed Services_](dependency-injection.md#keyed-services) (also called _Keyed DI_) is a DI feature that allows you to conveniently operate with multiple implementations of a single service. Upon registration, you can assign different _service keys_ to the implementations. In runtime, this key is used in lookup in combination with a service type, which means you can retrieve a specific implementation by passing the matching key. For more information on Keyed Services, and DI in general, see [.NET dependency injection](dependency-injection.md). + +For an overview on how to use `IHttpClientFactory` in your .NET application, see [IHttpClientFactory with .NET](httpclient-factory.md). + +## Background + +`IHttpClientFactory` and Named `HttpClient` instances, unsurprisingly, align well with the Keyed Services idea. Among other things, `IHttpClientFactory` was a way to overcome this long-missing DI feature. But plain Named clients require you to obtain, store and query the `IHttpClientFactory` instance — instead of injecting a configured `HttpClient` — which might be inconvenient. While Typed clients attempt to simplify that part, it comes with a catch: Typed clients are quite easy to [misconfigure](httpclient-factory-troubleshooting.md#typed-client-has-the-wrong-httpclient-injected) and [misuse](httpclient-factory.md#avoid-typed-clients-in-singleton-services), and the supporting infra can also be a tangible overhead in certain scenarios (for example, om mobile platforms). + +Starting from .NET 9 (package-provided), `IHttpClientFactory` can leverage Keyed DI directly, introducing a new "Keyed DI approach" (as opposed to "Named" and "Typed" approaches). "Keyed DI approach" pairs the convenient, highly configurable `HttpClient` registrations with the straightforward injection of the specific configured `HttpClient` instances. + +## Basic Usage + +As of .NET 9 (package-provided), you need to _opt in_ to the feature by calling the [`AddAsKeyed()`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addaskeyed) extension method. This will register a Named `HttpClient` as a Keyed service, using the client's name as a service key — and enable you to leverage the Keyed Services APIs (e.g. [`[FromKeyedServices(...)]`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute)) to obtain the required `HttpClient` instances. By default, the clients are registered with Scoped lifetime. + +Below is a full runnable example of the integration between `HttpClientFactory`, Keyed DI and ASP.NET Core 9.0 Minimal APIs: + +:::code source="snippets/http/keyedservices/Program.cs" highlight="9,14"::: + +Endpoint response: + +```sh +> ~ curl http://localhost:5000/ +{"name":"runtime","url":"https://api.github.com/repos/dotnet/runtime"} +``` + +In the example above, the configured `HttpClient` is directly injected into the request handler via the Keyed DI infra and ASP.NET Core parameter binding. For more information on Keyed Services in ASP.NET Core, see [Dependency injection in ASP.NET Core](/aspnet/core/fundamentals/dependency-injection#keyed-services) + +## Approach comparison + +Stripping away the code unrelated to `IHttpClientFactory`, you can compare the _Keyed DI approach_ + +```csharp +services.AddHttpClient("github", /* ... */).AddAsKeyed(); // (1) + +app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) => // (2) + //httpClient.Get.... // (3) +``` + +with how the same is achieved the two "older" ones: first with the _Named approach_ + +```csharp +services.AddHttpClient("github", /* ... */); // (1) + +app.MapGet("/github", (IHttpClientFactory httpClientFactory) => +{ + HttpClient httpClient = httpClientFactory.CreateClient("github"); // (2) + //return httpClient.Get.... // (3) +}); +``` + +and second, the _Typed approach_. + +```csharp +services.AddHttpClient(/* ... */); // (1) + +app.MapGet("/github", (GitHubClient gitHubClient) => + gitHubClient.GetRepoAsync()); + +public class GitHubClient(HttpClient httpClient) // (2) +{ + private readonly HttpClient _httpClient = httpClient; + + public Task GetRepoAsync() => + //_httpClient.Get.... // (3) +} +``` + +Out of the three, the Keyed DI approach offers the most succinct way to achieve the same behavior. + +## Keyed Message Handler Chain + +For some advanced scenarios, you might want to access `HttpMessageHandler` chain directly, instead of an `HttpClient` object. `HttpClientFactory` provides `IHttpMessageHandlerFactory` interface for that; and if you enable Keyed DI, then not only `HttpClient`, but also the respective `HttpMessageHandler` chain will be registered as a Keyed service: + +```csharp +services.AddHttpClient("foo").AddAsKeyed(); + +var handler = provider.GetRequiredKeyedService("foo"); +var invoker = new HttpMessageInvoker(handler, disposeHandler: false); +``` + + +## HOW-TO: Switch from Typed approach to Keyed DI + +> [!NOTE] +> We currently recommend using Keyed DI approach instead of Typed clients. + +A minimal-change switch from an existing Typed client to a Keyed dependency can look as follows: + +```diff +- services.AddHttpClient( // (1) Typed client ++ services.AddHttpClient(nameof(Foo), // (1) Named client + c => { /* ... */ } // HttpClient configuration + //).Configure.... +- ); ++ ).AddAsKeyed(); // (1) Keyed DI opt-in ++ ++ services.AddTransient(); // (1) Plain Transient service + + public class Foo( +- // (2) Implicit ("hidden" Named) dependency ++ [FromKeyedServices(nameof(Foo))] // (2) Explicit Keyed dependency + HttpClient httpClient) // { ... +``` + +In the example above: +1. The registration of the Typed client `Foo` is split into: + - A registration of a Named client `nameof(Foo)` + - retaining the `HttpClient` configuration, and + - opting in to Keyed DI; + - Plain Transient service `Foo`. +2. `HttpClient` dependency in `Foo`'s constructor is explicitly bound to a Keyed Service with a key `nameof(Foo)`. + +The name doesn't have to be `nameof(Foo)`, but the example aimed to minimize semantic change as well: Typed clients use Named clients "under the hood", and by default, such "hidden" Named clients go by the linked Typed client's type name; in this case it is `nameof(Foo)`. + +Technically, the example "unwraps" the Typed client, so that the previously "hidden" Named client becomes "exposed", and the dependency is satisfied via the Keyed DI infra instead of the Typed client infra. + + +## Leverage DI Container Validation + +If you enabled the Keyed registration for a specific Named client, you can access it with any existing Keyed DI APIs, but if you try to use a name which was not enabled yet, you will get the standard Keyed DI exception: + +```c# +services.AddHttpClient("keyed").AddAsKeyed(); +services.AddHttpClient("not-keyed"); + +provider.GetRequiredKeyedService("keyed"); // OK + +// Throws: No service for type 'System.Net.Http.HttpClient' has been registered. +provider.GetRequiredKeyedService("not-keyed"); +``` + +Additionaly, the Scoped lifetime of the clients can help to catch cases of captive dependencies: + +```csharp +services.AddHttpClient("scoped").AddAsKeyed(); +services.AddSingleton(); + +// Throws: Cannot resolve scoped service 'System.Net.Http.HttpClient' from root provider. +rootProvider.GetRequiredKeyedService("scoped"); + +using var scope = provider.CreateScope(); +scope.ServiceProvider.GetRequiredKeyedService("scoped"); // OK + +// Throws: Cannot consume scoped service 'System.Net.Http.HttpClient' from singleton 'CapturingSingleton'. +public class CapturingSingleton([FromKeyedServices("scoped")] HttpClient httpClient) +//{ ... +``` + + +## HOW-TO: Opt-In To Keyed DI By Default + +You don't have to call `AddAsKeyed` for every single client — you can easily opt in "globally" (for any client name) via `ConfigureHttpClientDefaults`. From Keyed Services perspective, it will result in the [`KeyedService.AnyKey`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.keyedservice.anykey) registration. + +```csharp +services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); + +services.AddHttpClient("foo", /* ... */); +services.AddHttpClient("bar", /* ... */); +services.AddHttpClient("baz", /* ... */); + +public class FooBarBazController( + [FromKeyedServices("foo")] HttpClient foo, + [FromKeyedServices("bar")] HttpClient bar, + [FromKeyedServices("baz")] HttpClient baz) +//{ ... +``` + +## ⚠️ Beware Of "Unknown" Clients ⚠️ + +One thing to be aware of with `KeyedService.AnyKey` registrations, is that a mistake in the key _silently_ leads to a wrong instance being injected. In case of Keyed `HttpClient`s, a mistake in the client name can result even in erroneously injecting an "unknown" client — meaning, a client which name was never registered. + +Actually, in the first place, same is true for the plain Named clients: `HttpClientFactory` doesn't require the client name to be explicitly registered (aligning with the way the [Options pattern](options.md) work). The factory will give you an unconfigured — or, more precisely, default-configured — `HttpClient` for any unknown name. + +Therefore, it is important to keep in mind: the "Keyed by default" approach covers not only all _registered_ `HttpClient`s, but all the clients that `HttpClientFactory` is _able to create_. + +```csharp +services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); +services.AddHttpClient("known", /* ... */); + +provider.GetRequiredKeyedService("known"); // OK +provider.GetRequiredKeyedService("unknown"); // OK (unconfigured instance) +``` + +## "Opt-In" Strategy Considerations + +Even though the "global" opt-in is a one-liner, it is unfortunate that the feature still requires it, instead of just working "out of the box". For full context and reasoning on that decision, see [dotnet/runtime#89755](https://github.com/dotnet/runtime/issues/89755) and [dotnet/runtime#104943](https://github.com/dotnet/runtime/pull/104943). In short, the main blocker for "on by default" is the `ServiceLifetime` "controversy": for the current (`9.0.0`) state of the DI and `HttpClientFactory` implementations, there is no single `ServiceLifetime` that would be reasonably safe for all `HttpClient`s in all possible situations. There is an intention, however, to address the caveats in the upcoming releases, and switch the strategy from "opt-in" to "opt-out". + +## HOW-TO: Opt-Out From Keyed Registration + +You can explicitly opt out from Keyed DI for `HttpClient`s by calling [`RemoveAsKeyed()`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.removeaskeyed), either per client name: + +```csharp +services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); // opt IN by default +services.AddHttpClient("keyed", /* ... */); +services.AddHttpClient("not-keyed", /* ... */).RemoveAsKeyed(); // opt OUT per name + +provider.GetRequiredKeyedService("keyed"); // OK +provider.GetRequiredKeyedService("not-keyed"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered. +provider.GetRequiredKeyedService("unknown"); // OK (unconfigured instance) +``` + +or "globally" with `ConfigureHttpClientDefaults`: + +```csharp +services.ConfigureHttpClientDefaults(b => b.RemoveAsKeyed()); // opt OUT by default +services.AddHttpClient("keyed", /* ... */).AddAsKeyed(); // opt IN per name +services.AddHttpClient("not-keyed", /* ... */); + +provider.GetRequiredKeyedService("keyed"); // OK +provider.GetRequiredKeyedService("not-keyed"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered. +provider.GetRequiredKeyedService("unknown"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered. +``` + +## Service Lifetime Selection + +By default, `AddAsKeyed()` registers `HttpClient` as a Keyed _Scoped_ service. You can also explicitly specify the lifetime by passing the `ServiceLifetime` parameter to the `AddAsKeyed()` method: + +```csharp +services.AddHttpClient("explicit-scoped") + .AddAsKeyed(ServiceLifetime.Scoped); + +services.AddHttpClient("singleton") + .AddAsKeyed(ServiceLifetime.Singleton); +``` + +## ⚠️ Avoid Transient HttpClient Memory Leak ⚠️ + +We strongly recommend _avoiding_ Transient lifetime for Keyed `HttpClient`s. + +Registering the client as a Keyed Transient service will lead to the `HttpClient` and `HttpMessageHandler` instances being _captured by DI container_, as both implement `IDisposable`. This can result in _memory leaks_ if the client is resolved multiple times within Singleton services. + +## ⚠️ Avoid Captive Dependency ⚠️ + +If `HttpClient` is registered either: + - as a Keyed _Singleton_, -OR- + - as a Keyed _Scoped_ or _Transient_, and injected within a _long-running_ (longer than `HandlerLifetime`) application Scope, -OR- + - as a Keyed _Transient_, and injected into a _Singleton_ service, + +the `HttpClient` instance will become _captive_, and will most possibly outlive way beyond its expected `HandlerLifetime`. `HttpClientFactory` has no control over captive clients, they are NOT able to participate in the handler rotation, and this can result in [the loss of DNS changes](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-dns-changes). A similar issue [already exists](httpclient-factory.md#avoid-typed-clients-in-singleton-services) for Typed clients, which are registered as Transient services. + +In cases when client's longevity cannot be avoided — or if it is concsiously desired, e.g. for a Keyed Singleton — it is advised to [leverage `SocketsHttpHandler`](httpclient-factory.md#using-ihttpclientfactory-together-with-socketshttphandler) by setting `PooledConnectionLifetime` to a reasonable value. + +```csharp +services.AddHttpClient("shared") + .AddAsKeyed(ServiceLifetime.Singleton) // explicit singleton + .UseSocketsHttpHandler((h, _) => h.PooledConnectionLifetime = TimeSpan.FromMinutes(2)) + .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // disable rotation +services.AddSingleton(); + +public class MySingleton([FromKeyedServices("shared")] HttpClient shared) // { ... +``` + +## ⚠️ Beware Of Scope Mismatch ⚠️ + +While Scoped lifetime is much less problematic for the Named `HttpClient`s (compared to Singleton and Transient pitfalls), it has its own catch. + +Keyed Scoped lifetime of a specific `HttpClient` instance will be bound — as expected — to the "ordinary" application scope (e.g. incoming request scope) where it was resolved from. However, it does NOT apply to the underlying message handler chain, which is still managed by the `HttpClientFactory`, in the same way it is for the Named clients created directly from factory. `HttpClient`s with the _same_ name, but resolved (within a `HandlerLifetime` timeframe) in two different scopes (e.g. two concurrent requests to the same endpoint), can reuse the _same_ `HttpMessageHandler` instance. Which in turn will have it's own separate scope, as illustrated in the [docs](httpclient-factory.md#message-handler-scopes-in-ihttpclientfactory). + +The [Scope Mismatch](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-scoped-lifetime) problem is nasty and long-existing one, and as of .NET 9 is still remains [unsolved](https://github.com/dotnet/runtime/issues/47091). From a service injected through the regular DI infra, you would expect all the dependencies to be satisfied from the same scope — but be aware that for the Keyed Scoped `HttpClient`s it is, unfortunately, not the case. + +## Precedence + +If called together or any of them more then once, `AddAsKeyed()` and `RemoveAsKeyed()` generally follow the rules of `HttpClientFactory` configs and DI registrations: + +1. If called for the same name, the last one wins: the lifetime from the last `AddAsKeyed()` is used to create the Keyed registration (unless `RemoveAsKeyed()` was called last, in which case the name is excluded). +2. If used only within `ConfigureHttpClientDefaults`, the last one wins. +3. If both `ConfigureHttpClientDefaults` and specific client name were used, all defaults are considered to "happen" before all per-name ones. Thus, defaults can be disregarded, and the last of the per-name ones wins. + +Note that if `AddAsKeyed()` is called within a Typed client registration, only the underlying Named client will be registered as Keyed. The Typed client itself will continue to be registered as a plain Transient service. + +## See also + +- [IHttpClientFactory with .NET][hcf] +- + +[hcf]: httpclient-factory.md +[httpclient]: ../../fundamentals/networking/http/httpclient.md + +----------------- + +# Default Primary Handler Change + +One of the most common problems `HttpClientFactory` users run into is when a Named or a Typed client erroneously gets [captured in a Singleton service](httpclient-factory.md#avoid-typed-clients-in-singleton-services), or, in general, stored somewhere for a period of time that's longer than the specified `HandlerLifetime`. Because `HttpClientFactory` can't rotate such handlers, they might end up [not respecting DNS changes](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-dns-changes). It is, unfortunately, very easy and seemingly "intuitive" to inject a Typed client into a singleton, but very hard to have any kind of check/analyzer to make sure `HttpClient` is not captured when it was not supposed to. It might be even harder to troubleshoot the resulting issues. + +On the other hand, the problem can be [mitigated](httpclient-factory.md#using-ihttpclientfactory-together-with-socketshttphandler) by using `SocketsHttpHandler`, which has an option to control `PooledConnectionLifetime`. Similarly to `HandlerLifetime`, it allows regularly recreating connections to pick up the DNS changes, but on a lower level. A client with `PooledConnectionLifetime` set up can be safely used as a Singleton. + +Therefore, to minimize the potential impact of the erroneous usage patterns, .NET 9 makes the default Primary handler to be a `SocketsHttpHandler` (on platforms that support it; other platforms, e.g. .NET Framework, continue to use `HttpClientHandler`). And most importantly, `SocketsHttpHandler` will also have the `PooledConnectionLifetime` property _pre-set_ to match the `HandlerLifetime` value (it will reflect the latest value, if you configured `HandlerLifetime` one or more times). + +Note that the change will only affect cases when the client was _not_ configured to have a custom Primary handler (via e.g. `ConfigurePrimaryHttpMessageHandler()`). + +While the default Primary handler is an implementation detail, as it is never specified in the docs, it is still considered a breaking change. There are cases in which you might have depended on it, for example, casting the Primary handler to `HttpClientHandler` to set properties like `ClientCertificates`, `UseCookies`, `UseProxy` etc. If you need to use such properties, it is suggected to check for both `HttpClientHandler` and `SocketsHttpHandler` in the configuration action: + +```csharp +services.AddHttpClient("test") + .ConfigurePrimaryHttpMessageHandler((h, _) => + { + if (h is HttpClientHandler hch) + { + hch.UseCookies = false; + } + + if (h is SocketsHttpHandler shh) + { + shh.UseCookies = false; + } + }); +``` + +Alternatively, you can explicitly specify a Primary handler for each of your clients: + +```csharp +services.AddHttpClient("test") + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false }); +``` + +Or, configure the default Primary handler for all clients using `ConfigureHttpClientDefaults`: + +```csharp +services.ConfigureHttpClientDefaults(b => + b.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false })); +``` + + + + + + diff --git a/docs/core/extensions/snippets/http/keyedservices/Program.cs b/docs/core/extensions/snippets/http/keyedservices/Program.cs new file mode 100644 index 0000000000000..c648dab21f278 --- /dev/null +++ b/docs/core/extensions/snippets/http/keyedservices/Program.cs @@ -0,0 +1,22 @@ +var builder = WebApplication.CreateBuilder(args); + +// --- (1) Registration --- +builder.Services.AddHttpClient("github", c => + { + c.BaseAddress = new Uri("https://api.github.com/"); + c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); + c.DefaultRequestHeaders.Add("User-Agent", "dotnet"); + }) + .AddAsKeyed(); // Add HttpClient as a Keyed Scoped service for key="github" + +var app = builder.Build(); + +// --- (2) Obtaining HttpClient instance --- +// Directly inject the Keyed HttpClient by its name +app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) => + // --- (3) Using HttpClient instance --- + httpClient.GetFromJsonAsync("/repos/dotnet/runtime")); + +app.Run(); + +record Repo(string Name, string Url); diff --git a/docs/core/extensions/snippets/http/keyedservices/keyedservices.csproj b/docs/core/extensions/snippets/http/keyedservices/keyedservices.csproj new file mode 100644 index 0000000000000..f5c993c757d95 --- /dev/null +++ b/docs/core/extensions/snippets/http/keyedservices/keyedservices.csproj @@ -0,0 +1,10 @@ + + + + net9.0 + enable + enable + KeyedServices + + + From 145b960a2b04439f9ac4e7713eee0cbefcf0edfe Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Tue, 21 Jan 2025 13:59:21 +0000 Subject: [PATCH 2/9] (WIP) --- .../extensions/httpclient-factory-keyed-di.md | 245 +++++++----------- docs/core/extensions/httpclient-factory.md | 73 ++++++ 2 files changed, 172 insertions(+), 146 deletions(-) diff --git a/docs/core/extensions/httpclient-factory-keyed-di.md b/docs/core/extensions/httpclient-factory-keyed-di.md index 6fa73ad7a1c39..7bc929b7e5554 100644 --- a/docs/core/extensions/httpclient-factory-keyed-di.md +++ b/docs/core/extensions/httpclient-factory-keyed-di.md @@ -16,7 +16,7 @@ For an overview on how to use `IHttpClientFactory` in your .NET application, see ## Background -`IHttpClientFactory` and Named `HttpClient` instances, unsurprisingly, align well with the Keyed Services idea. Among other things, `IHttpClientFactory` was a way to overcome this long-missing DI feature. But plain Named clients require you to obtain, store and query the `IHttpClientFactory` instance — instead of injecting a configured `HttpClient` — which might be inconvenient. While Typed clients attempt to simplify that part, it comes with a catch: Typed clients are quite easy to [misconfigure](httpclient-factory-troubleshooting.md#typed-client-has-the-wrong-httpclient-injected) and [misuse](httpclient-factory.md#avoid-typed-clients-in-singleton-services), and the supporting infra can also be a tangible overhead in certain scenarios (for example, om mobile platforms). +`IHttpClientFactory` and Named `HttpClient` instances, unsurprisingly, align well with the Keyed Services idea. Among other things, `IHttpClientFactory` historically was a way to overcome this long-missing DI feature. But plain Named clients require you to obtain, store and query the `IHttpClientFactory` instance — instead of injecting a configured `HttpClient` — which might be inconvenient. While Typed clients attempt to simplify that part, it comes with a catch: Typed clients are quite easy to [misconfigure](httpclient-factory-troubleshooting.md#typed-client-has-the-wrong-httpclient-injected) and [misuse](httpclient-factory.md#avoid-typed-clients-in-singleton-services), and the supporting infra can also be a tangible overhead in certain scenarios (for example, om mobile platforms). Starting from .NET 9 (package-provided), `IHttpClientFactory` can leverage Keyed DI directly, introducing a new "Keyed DI approach" (as opposed to "Named" and "Typed" approaches). "Keyed DI approach" pairs the convenient, highly configurable `HttpClient` registrations with the straightforward injection of the specific configured `HttpClient` instances. @@ -26,7 +26,7 @@ As of .NET 9 (package-provided), you need to _opt in_ to the feature by calling Below is a full runnable example of the integration between `HttpClientFactory`, Keyed DI and ASP.NET Core 9.0 Minimal APIs: -:::code source="snippets/http/keyedservices/Program.cs" highlight="9,14"::: +:::code source="snippets/http/keyedservices/Program.cs" highlight="4,10,16"::: Endpoint response: @@ -37,7 +37,7 @@ Endpoint response: In the example above, the configured `HttpClient` is directly injected into the request handler via the Keyed DI infra and ASP.NET Core parameter binding. For more information on Keyed Services in ASP.NET Core, see [Dependency injection in ASP.NET Core](/aspnet/core/fundamentals/dependency-injection#keyed-services) -## Approach comparison +## Approach Comparison Stripping away the code unrelated to `IHttpClientFactory`, you can compare the _Keyed DI approach_ @@ -79,6 +79,84 @@ public class GitHubClient(HttpClient httpClient) // (2) Out of the three, the Keyed DI approach offers the most succinct way to achieve the same behavior. +## Built-In DI Container Validation + +If you enabled the Keyed registration for a specific Named client, you can access it with any existing Keyed DI APIs. But if you erroneously try to use a name which was not enabled yet, you will get the standard Keyed DI exception: + +```c# +services.AddHttpClient("keyed").AddAsKeyed(); +services.AddHttpClient("not-keyed"); + +provider.GetRequiredKeyedService("keyed"); // OK + +// Throws: No service for type 'System.Net.Http.HttpClient' has been registered. +provider.GetRequiredKeyedService("not-keyed"); +``` + +Additionaly, the Scoped lifetime of the clients can help catching cases of captive dependencies: + +```csharp +services.AddHttpClient("scoped").AddAsKeyed(); +services.AddSingleton(); + +// Throws: Cannot resolve scoped service 'System.Net.Http.HttpClient' from root provider. +rootProvider.GetRequiredKeyedService("scoped"); + +using var scope = provider.CreateScope(); +scope.ServiceProvider.GetRequiredKeyedService("scoped"); // OK + +// Throws: Cannot consume scoped service 'System.Net.Http.HttpClient' from singleton 'CapturingSingleton'. +public class CapturingSingleton([FromKeyedServices("scoped")] HttpClient httpClient) +//{ ... +``` + +## Service Lifetime Selection + +By default, `AddAsKeyed()` registers `HttpClient` as a Keyed _Scoped_ service. You can also explicitly specify the lifetime by passing the `ServiceLifetime` parameter to the `AddAsKeyed()` method: + +```csharp +services.AddHttpClient("explicit-scoped") + .AddAsKeyed(ServiceLifetime.Scoped); + +services.AddHttpClient("singleton") + .AddAsKeyed(ServiceLifetime.Singleton); +``` + +### ⚠️ Avoid Transient HttpClient Memory Leak ⚠️ + +We strongly recommend _avoiding_ Transient lifetime for Keyed `HttpClient`s. + +Registering the client as a Keyed Transient service will lead to the `HttpClient` and `HttpMessageHandler` instances being _captured by DI container_, as both implement `IDisposable`. This can result in _memory leaks_ if the client is resolved multiple times within Singleton services. + +### ⚠️ Avoid Captive Dependency ⚠️ + +If `HttpClient` is registered either: + - as a Keyed _Singleton_, -OR- + - as a Keyed _Scoped_ or _Transient_, and injected within a _long-running_ (longer than `HandlerLifetime`) application Scope, -OR- + - as a Keyed _Transient_, and injected into a _Singleton_ service, + +the `HttpClient` instance will become _captive_, and will most possibly outlive way beyond its expected `HandlerLifetime`. `HttpClientFactory` has no control over captive clients, they are NOT able to participate in the handler rotation, and this can result in [the loss of DNS changes](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-dns-changes). A similar issue [already exists](httpclient-factory.md#avoid-typed-clients-in-singleton-services) for Typed clients, which are registered as Transient services. + +In cases when client's longevity cannot be avoided — or if it is concsiously desired, e.g. for a Keyed Singleton — it is advised to [leverage `SocketsHttpHandler`](httpclient-factory.md#using-ihttpclientfactory-together-with-socketshttphandler) by setting `PooledConnectionLifetime` to a reasonable value. + +```csharp +services.AddHttpClient("shared") + .AddAsKeyed(ServiceLifetime.Singleton) // explicit singleton + .UseSocketsHttpHandler((h, _) => h.PooledConnectionLifetime = TimeSpan.FromMinutes(2)) + .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // disable rotation +services.AddSingleton(); + +public class MySingleton([FromKeyedServices("shared")] HttpClient shared) // { ... +``` + +### ⚠️ Beware Of Scope Mismatch ⚠️ + +While Scoped lifetime is much less problematic for the Named `HttpClient`s (compared to Singleton and Transient pitfalls), it has its own catch. + +Keyed Scoped lifetime of a specific `HttpClient` instance will be bound — as expected — to the "ordinary" application scope (e.g. incoming request scope) where it was resolved from. However, it does NOT apply to the underlying message handler chain, which is still managed by the `HttpClientFactory`, in the same way it is for the Named clients created directly from factory. `HttpClient`s with the _same_ name, but resolved (within a `HandlerLifetime` timeframe) in two different scopes (e.g. two concurrent requests to the same endpoint), can reuse the _same_ `HttpMessageHandler` instance. Which in turn will have it's own separate scope, as illustrated in the [docs](httpclient-factory.md#message-handler-scopes-in-ihttpclientfactory). + +The [Scope Mismatch](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-scoped-lifetime) problem is nasty and long-existing one, and as of .NET 9 (package-provided) is still remains [unsolved](https://github.com/dotnet/runtime/issues/47091). From a service injected through the regular DI infra, you would expect all the dependencies to be satisfied from the same scope — but be aware that for the Keyed Scoped `HttpClient`s it is, unfortunately, not the case. + ## Keyed Message Handler Chain For some advanced scenarios, you might want to access `HttpMessageHandler` chain directly, instead of an `HttpClient` object. `HttpClientFactory` provides `IHttpMessageHandlerFactory` interface for that; and if you enable Keyed DI, then not only `HttpClient`, but also the respective `HttpMessageHandler` chain will be registered as a Keyed service: @@ -90,7 +168,6 @@ var handler = provider.GetRequiredKeyedService("foo"); var invoker = new HttpMessageInvoker(handler, disposeHandler: false); ``` - ## HOW-TO: Switch from Typed approach to Keyed DI > [!NOTE] @@ -104,61 +181,26 @@ A minimal-change switch from an existing Typed client to a Keyed dependency can c => { /* ... */ } // HttpClient configuration //).Configure.... - ); -+ ).AddAsKeyed(); // (1) Keyed DI opt-in -+ ++ ).AddAsKeyed(); // (1) + Keyed DI opt-in + + services.AddTransient(); // (1) Plain Transient service public class Foo( - // (2) Implicit ("hidden" Named) dependency -+ [FromKeyedServices(nameof(Foo))] // (2) Explicit Keyed dependency ++ [FromKeyedServices(nameof(Foo))] // (2) Explicit Keyed Service dependency HttpClient httpClient) // { ... ``` In the example above: 1. The registration of the Typed client `Foo` is split into: - - A registration of a Named client `nameof(Foo)` - - retaining the `HttpClient` configuration, and - - opting in to Keyed DI; + - A registration of a Named client `nameof(Foo)` with the same `HttpClient` configuration, and opt-in to Keyed DI; and - Plain Transient service `Foo`. -2. `HttpClient` dependency in `Foo`'s constructor is explicitly bound to a Keyed Service with a key `nameof(Foo)`. +2. `HttpClient` dependency in `Foo` is explicitly bound to a Keyed Service with a key `nameof(Foo)`. -The name doesn't have to be `nameof(Foo)`, but the example aimed to minimize semantic change as well: Typed clients use Named clients "under the hood", and by default, such "hidden" Named clients go by the linked Typed client's type name; in this case it is `nameof(Foo)`. +The name doesn't have to be `nameof(Foo)`, but the example aimed to minimize the behavioral changes. Typed clients use Named clients "under the hood", and by default, such "hidden" Named clients go by the linked Typed client's type name. In this case, the "hidden" name was `nameof(Foo)`, so the example preserved it. Technically, the example "unwraps" the Typed client, so that the previously "hidden" Named client becomes "exposed", and the dependency is satisfied via the Keyed DI infra instead of the Typed client infra. - -## Leverage DI Container Validation - -If you enabled the Keyed registration for a specific Named client, you can access it with any existing Keyed DI APIs, but if you try to use a name which was not enabled yet, you will get the standard Keyed DI exception: - -```c# -services.AddHttpClient("keyed").AddAsKeyed(); -services.AddHttpClient("not-keyed"); - -provider.GetRequiredKeyedService("keyed"); // OK - -// Throws: No service for type 'System.Net.Http.HttpClient' has been registered. -provider.GetRequiredKeyedService("not-keyed"); -``` - -Additionaly, the Scoped lifetime of the clients can help to catch cases of captive dependencies: - -```csharp -services.AddHttpClient("scoped").AddAsKeyed(); -services.AddSingleton(); - -// Throws: Cannot resolve scoped service 'System.Net.Http.HttpClient' from root provider. -rootProvider.GetRequiredKeyedService("scoped"); - -using var scope = provider.CreateScope(); -scope.ServiceProvider.GetRequiredKeyedService("scoped"); // OK - -// Throws: Cannot consume scoped service 'System.Net.Http.HttpClient' from singleton 'CapturingSingleton'. -public class CapturingSingleton([FromKeyedServices("scoped")] HttpClient httpClient) -//{ ... -``` - - ## HOW-TO: Opt-In To Keyed DI By Default You don't have to call `AddAsKeyed` for every single client — you can easily opt in "globally" (for any client name) via `ConfigureHttpClientDefaults`. From Keyed Services perspective, it will result in the [`KeyedService.AnyKey`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.keyedservice.anykey) registration. @@ -177,13 +219,17 @@ public class FooBarBazController( //{ ... ``` -## ⚠️ Beware Of "Unknown" Clients ⚠️ +### ⚠️ Beware Of "Unknown" Clients ⚠️ + +`KeyedService.AnyKey` registrations define a mapping from _any_ key value to some service instance. However, as a result, the Container validation will not apply, and an _erroneous_ key value will _silently_ lead to a _wrong instance_ being injected. -One thing to be aware of with `KeyedService.AnyKey` registrations, is that a mistake in the key _silently_ leads to a wrong instance being injected. In case of Keyed `HttpClient`s, a mistake in the client name can result even in erroneously injecting an "unknown" client — meaning, a client which name was never registered. +> [!IMPORTANT] +> In case of Keyed `HttpClient`s, a mistake in the client name can result in erroneously injecting an "unknown" client — meaning, a client which name was never registered. -Actually, in the first place, same is true for the plain Named clients: `HttpClientFactory` doesn't require the client name to be explicitly registered (aligning with the way the [Options pattern](options.md) work). The factory will give you an unconfigured — or, more precisely, default-configured — `HttpClient` for any unknown name. +Actually, in the first place, same is true for the plain Named clients: `IHttpClientFactory` doesn't require the client name to be explicitly registered (aligning with the way the [Options pattern](options.md) work). The factory will give you an unconfigured — or, more precisely, default-configured — `HttpClient` for any unknown name. -Therefore, it is important to keep in mind: the "Keyed by default" approach covers not only all _registered_ `HttpClient`s, but all the clients that `HttpClientFactory` is _able to create_. +> [!NOTE] +> Therefore, it is important to keep in mind: the "Keyed by default" approach covers not only all _registered_ `HttpClient`s, but all the clients that `HttpClientFactory` is _able to create_. ```csharp services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); @@ -193,7 +239,7 @@ provider.GetRequiredKeyedService("known"); // OK provider.GetRequiredKeyedService("unknown"); // OK (unconfigured instance) ``` -## "Opt-In" Strategy Considerations +### "Opt-In" Strategy Considerations Even though the "global" opt-in is a one-liner, it is unfortunate that the feature still requires it, instead of just working "out of the box". For full context and reasoning on that decision, see [dotnet/runtime#89755](https://github.com/dotnet/runtime/issues/89755) and [dotnet/runtime#104943](https://github.com/dotnet/runtime/pull/104943). In short, the main blocker for "on by default" is the `ServiceLifetime` "controversy": for the current (`9.0.0`) state of the DI and `HttpClientFactory` implementations, there is no single `ServiceLifetime` that would be reasonably safe for all `HttpClient`s in all possible situations. There is an intention, however, to address the caveats in the upcoming releases, and switch the strategy from "opt-in" to "opt-out". @@ -223,54 +269,8 @@ provider.GetRequiredKeyedService("not-keyed"); // Throws: No service provider.GetRequiredKeyedService("unknown"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered. ``` -## Service Lifetime Selection -By default, `AddAsKeyed()` registers `HttpClient` as a Keyed _Scoped_ service. You can also explicitly specify the lifetime by passing the `ServiceLifetime` parameter to the `AddAsKeyed()` method: - -```csharp -services.AddHttpClient("explicit-scoped") - .AddAsKeyed(ServiceLifetime.Scoped); - -services.AddHttpClient("singleton") - .AddAsKeyed(ServiceLifetime.Singleton); -``` - -## ⚠️ Avoid Transient HttpClient Memory Leak ⚠️ - -We strongly recommend _avoiding_ Transient lifetime for Keyed `HttpClient`s. - -Registering the client as a Keyed Transient service will lead to the `HttpClient` and `HttpMessageHandler` instances being _captured by DI container_, as both implement `IDisposable`. This can result in _memory leaks_ if the client is resolved multiple times within Singleton services. - -## ⚠️ Avoid Captive Dependency ⚠️ - -If `HttpClient` is registered either: - - as a Keyed _Singleton_, -OR- - - as a Keyed _Scoped_ or _Transient_, and injected within a _long-running_ (longer than `HandlerLifetime`) application Scope, -OR- - - as a Keyed _Transient_, and injected into a _Singleton_ service, - -the `HttpClient` instance will become _captive_, and will most possibly outlive way beyond its expected `HandlerLifetime`. `HttpClientFactory` has no control over captive clients, they are NOT able to participate in the handler rotation, and this can result in [the loss of DNS changes](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-dns-changes). A similar issue [already exists](httpclient-factory.md#avoid-typed-clients-in-singleton-services) for Typed clients, which are registered as Transient services. - -In cases when client's longevity cannot be avoided — or if it is concsiously desired, e.g. for a Keyed Singleton — it is advised to [leverage `SocketsHttpHandler`](httpclient-factory.md#using-ihttpclientfactory-together-with-socketshttphandler) by setting `PooledConnectionLifetime` to a reasonable value. - -```csharp -services.AddHttpClient("shared") - .AddAsKeyed(ServiceLifetime.Singleton) // explicit singleton - .UseSocketsHttpHandler((h, _) => h.PooledConnectionLifetime = TimeSpan.FromMinutes(2)) - .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // disable rotation -services.AddSingleton(); - -public class MySingleton([FromKeyedServices("shared")] HttpClient shared) // { ... -``` - -## ⚠️ Beware Of Scope Mismatch ⚠️ - -While Scoped lifetime is much less problematic for the Named `HttpClient`s (compared to Singleton and Transient pitfalls), it has its own catch. - -Keyed Scoped lifetime of a specific `HttpClient` instance will be bound — as expected — to the "ordinary" application scope (e.g. incoming request scope) where it was resolved from. However, it does NOT apply to the underlying message handler chain, which is still managed by the `HttpClientFactory`, in the same way it is for the Named clients created directly from factory. `HttpClient`s with the _same_ name, but resolved (within a `HandlerLifetime` timeframe) in two different scopes (e.g. two concurrent requests to the same endpoint), can reuse the _same_ `HttpMessageHandler` instance. Which in turn will have it's own separate scope, as illustrated in the [docs](httpclient-factory.md#message-handler-scopes-in-ihttpclientfactory). - -The [Scope Mismatch](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-scoped-lifetime) problem is nasty and long-existing one, and as of .NET 9 is still remains [unsolved](https://github.com/dotnet/runtime/issues/47091). From a service injected through the regular DI infra, you would expect all the dependencies to be satisfied from the same scope — but be aware that for the Keyed Scoped `HttpClient`s it is, unfortunately, not the case. - -## Precedence +## Order Of Precedence If called together or any of them more then once, `AddAsKeyed()` and `RemoveAsKeyed()` generally follow the rules of `HttpClientFactory` configs and DI registrations: @@ -283,57 +283,10 @@ Note that if `AddAsKeyed()` is called within a Typed client registration, only t ## See also - [IHttpClientFactory with .NET][hcf] +- [Common `IHttpClientFactory` usage issues][hcf-troubleshooting] - [hcf]: httpclient-factory.md +[hcf-troubleshooting]: httpclient-factory-troubleshooting.md [httpclient]: ../../fundamentals/networking/http/httpclient.md ------------------ - -# Default Primary Handler Change - -One of the most common problems `HttpClientFactory` users run into is when a Named or a Typed client erroneously gets [captured in a Singleton service](httpclient-factory.md#avoid-typed-clients-in-singleton-services), or, in general, stored somewhere for a period of time that's longer than the specified `HandlerLifetime`. Because `HttpClientFactory` can't rotate such handlers, they might end up [not respecting DNS changes](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-dns-changes). It is, unfortunately, very easy and seemingly "intuitive" to inject a Typed client into a singleton, but very hard to have any kind of check/analyzer to make sure `HttpClient` is not captured when it was not supposed to. It might be even harder to troubleshoot the resulting issues. - -On the other hand, the problem can be [mitigated](httpclient-factory.md#using-ihttpclientfactory-together-with-socketshttphandler) by using `SocketsHttpHandler`, which has an option to control `PooledConnectionLifetime`. Similarly to `HandlerLifetime`, it allows regularly recreating connections to pick up the DNS changes, but on a lower level. A client with `PooledConnectionLifetime` set up can be safely used as a Singleton. - -Therefore, to minimize the potential impact of the erroneous usage patterns, .NET 9 makes the default Primary handler to be a `SocketsHttpHandler` (on platforms that support it; other platforms, e.g. .NET Framework, continue to use `HttpClientHandler`). And most importantly, `SocketsHttpHandler` will also have the `PooledConnectionLifetime` property _pre-set_ to match the `HandlerLifetime` value (it will reflect the latest value, if you configured `HandlerLifetime` one or more times). - -Note that the change will only affect cases when the client was _not_ configured to have a custom Primary handler (via e.g. `ConfigurePrimaryHttpMessageHandler()`). - -While the default Primary handler is an implementation detail, as it is never specified in the docs, it is still considered a breaking change. There are cases in which you might have depended on it, for example, casting the Primary handler to `HttpClientHandler` to set properties like `ClientCertificates`, `UseCookies`, `UseProxy` etc. If you need to use such properties, it is suggected to check for both `HttpClientHandler` and `SocketsHttpHandler` in the configuration action: - -```csharp -services.AddHttpClient("test") - .ConfigurePrimaryHttpMessageHandler((h, _) => - { - if (h is HttpClientHandler hch) - { - hch.UseCookies = false; - } - - if (h is SocketsHttpHandler shh) - { - shh.UseCookies = false; - } - }); -``` - -Alternatively, you can explicitly specify a Primary handler for each of your clients: - -```csharp -services.AddHttpClient("test") - .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false }); -``` - -Or, configure the default Primary handler for all clients using `ConfigureHttpClientDefaults`: - -```csharp -services.ConfigureHttpClientDefaults(b => - b.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false })); -``` - - - - - - diff --git a/docs/core/extensions/httpclient-factory.md b/docs/core/extensions/httpclient-factory.md index 3c1d98af119bb..7d5ff7cfb52e2 100644 --- a/docs/core/extensions/httpclient-factory.md +++ b/docs/core/extensions/httpclient-factory.md @@ -293,6 +293,79 @@ A further workaround can follow with an extension method for registering a scope For more information, see the [full example](https://github.com/dotnet/docs/tree/main/docs/core/extensions/snippets/http/scopeworkaround). +## Avoid depending on "factory-default" Primary Handler + +In this section, we use the term _"factory-default" Primary Handler_, by which we mean the Primary Handler that the default `IHttpClientFactory` implementation (or more precisely, the default `HttpMessageHandlerBuilder` implementation) will assign if _not configured in any way_ whatsoever. + +> [!NOTE] +> The "factory-default" Primary Handler is an _implementation detail_ and subject to change. +> It is strongly advised to AVOID depending on a specific implementation being used as a "factory-default" (e.g. `HttpClientHandler`). + +There are cases in which you need to depended on the specific type of a Primary Handler, especially if working on a class library. While preserving the end-user's configuration, you might want to update, for example, `HttpClientHandler`-specific properies like `ClientCertificates`, `UseCookies`, `UseProxy`, etc. + +It might be tempting to simply cast the Primary handler to `HttpClientHandler`, which _happened to_ work while `HttpClientHandler` was used as the "factory-default" Primary Handler. But as any code depending on implementation details, such a workaround is _fragile_ and bound to break. + +It is advised to leverage `ConfigureHttpClientDefaults` to set up an "app-level" default Primary Handler instance instead of relying on the "factory-default" one: + +```csharp +// Contract with the end-user: Only HttpClientHandler is supported. + +// --- "Pre-configure" stage --- +// The default is fixed as HttpClientHandler to avoid depending on the "factory-default" +// Primary Handler. +services.ConfigureHttpClientDefaults(b => + b.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false })); + +// --- "End-user" stage --- +// IHttpClientBuilder builder = services.AddHttpClient("test", /* ... */); +// ... + +// --- "Post-configure" stage --- +// The code can rely on the contract, and cast to HttpClientHandler only. +builder.ConfigurePrimaryHttpMessageHandler((handler, provider) => + { + if (handler is not HttpClientHandler h) + { + throw new InvalidOperationException("Only HttpClientHandler is supported"); + } + + h.ClientCertificates.Add(GetClientCert(provider, builder.Name)); + + //X509Certificate2 GetClientCert(IServiceProvider p, string name) { ... } + }); +``` + +Alternatively, you can consider checking the Primary Handler type, and configure the specifics like client certificates only in the well-known supporting types (most likely, `HttpClientHandler` and `SocketsHttpHandler`): + +```csharp +// --- "End-user" stage --- +// IHttpClientBuilder builder = services.AddHttpClient("test", /* ... */); +// ... + +// --- "Post-configure" stage --- +// No contract is in place. Trying to configure main handler types supporting client +// certs, logging and skipping otherwise. +builder.ConfigurePrimaryHttpMessageHandler((handler, provider) => + { + if (handler is HttpClientHandler h) + { + h.ClientCertificates.Add(GetClientCert(provider, builder.Name)); + } + else if (handler is SocketsHttpHandler s) + { + s.SslOptions ??= new System.Net.Security.SslClientAuthenticationOptions(); + s.SslOptions.ClientCertificates ??= new X509CertificateCollection(); + s.SslOptions.ClientCertificates!.Add(GetClientCert(provider, builder.Name)); + } + else + { + // Log warning + } + + //X509Certificate2 GetClientCert(IServiceProvider p, string name) { ... } + }); +``` + ## See also - [Common `IHttpClientFactory` usage issues][hcf-issues] From 90e904ce5eabc771d1e33a58fc97601f7ea00157 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Mon, 27 Jan 2025 13:24:51 +0000 Subject: [PATCH 3/9] add xrefs and toc --- .../extensions/httpclient-factory-keyed-di.md | 73 +++++++++++-------- docs/fundamentals/toc.yml | 3 + 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/docs/core/extensions/httpclient-factory-keyed-di.md b/docs/core/extensions/httpclient-factory-keyed-di.md index 7bc929b7e5554..4e814ad50a13a 100644 --- a/docs/core/extensions/httpclient-factory-keyed-di.md +++ b/docs/core/extensions/httpclient-factory-keyed-di.md @@ -3,28 +3,28 @@ title: Keyed DI Support in IHttpClientFactory description: Learn how to integrate IHttpClientFactory with Keyed Services. author: CarnaViire ms.author: knatalia -ms.date: 01/03/2025 +ms.date: 01/27/2025 --- # Keyed DI Support in `IHttpClientFactory` In this article, you'll learn how to integrate `IHttpClientFactory` with Keyed Services. -[_Keyed Services_](dependency-injection.md#keyed-services) (also called _Keyed DI_) is a DI feature that allows you to conveniently operate with multiple implementations of a single service. Upon registration, you can assign different _service keys_ to the implementations. In runtime, this key is used in lookup in combination with a service type, which means you can retrieve a specific implementation by passing the matching key. For more information on Keyed Services, and DI in general, see [.NET dependency injection](dependency-injection.md). +[_Keyed Services_](dependency-injection.md#keyed-services) (also called _Keyed DI_) is a DI feature that allows you to conveniently operate with multiple implementations of a single service. Upon registration, you can associate different _service keys_ with the specific implementations. In runtime, this key is used in lookup in combination with a service type, which means you can retrieve a specific implementation by passing the matching key. For more information on Keyed Services, and DI in general, see [.NET dependency injection][di]. -For an overview on how to use `IHttpClientFactory` in your .NET application, see [IHttpClientFactory with .NET](httpclient-factory.md). +For an overview on how to use `IHttpClientFactory` in your .NET application, see [IHttpClientFactory with .NET][hcf]. ## Background -`IHttpClientFactory` and Named `HttpClient` instances, unsurprisingly, align well with the Keyed Services idea. Among other things, `IHttpClientFactory` historically was a way to overcome this long-missing DI feature. But plain Named clients require you to obtain, store and query the `IHttpClientFactory` instance — instead of injecting a configured `HttpClient` — which might be inconvenient. While Typed clients attempt to simplify that part, it comes with a catch: Typed clients are quite easy to [misconfigure](httpclient-factory-troubleshooting.md#typed-client-has-the-wrong-httpclient-injected) and [misuse](httpclient-factory.md#avoid-typed-clients-in-singleton-services), and the supporting infra can also be a tangible overhead in certain scenarios (for example, om mobile platforms). +`IHttpClientFactory` and Named `HttpClient` instances, unsurprisingly, align well with the Keyed Services idea. Historically, among other things, `IHttpClientFactory` was a way to overcome this long-missing DI feature. But plain Named clients require you to obtain, store and query the `IHttpClientFactory` instance — instead of injecting a configured `HttpClient` — which might be inconvenient. While Typed clients attempt to simplify that part, it comes with a catch: Typed clients are quite easy to [misconfigure](httpclient-factory-troubleshooting.md#typed-client-has-the-wrong-httpclient-injected) and [misuse](httpclient-factory.md#avoid-typed-clients-in-singleton-services), and the supporting infra can also be a tangible overhead in certain scenarios (for example, om mobile platforms). -Starting from .NET 9 (package-provided), `IHttpClientFactory` can leverage Keyed DI directly, introducing a new "Keyed DI approach" (as opposed to "Named" and "Typed" approaches). "Keyed DI approach" pairs the convenient, highly configurable `HttpClient` registrations with the straightforward injection of the specific configured `HttpClient` instances. +Starting from .NET 9 (`Microsoft.Extensions.Http` and `Microsoft.Extensions.DependencyInjection` packages version `9.0.0+`), `IHttpClientFactory` can leverage Keyed DI directly, introducing a new "Keyed DI approach" (as opposed to "Named" and "Typed" approaches). "Keyed DI approach" pairs the convenient, highly configurable `HttpClient` registrations with the straightforward injection of the specific configured `HttpClient` instances. ## Basic Usage -As of .NET 9 (package-provided), you need to _opt in_ to the feature by calling the [`AddAsKeyed()`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addaskeyed) extension method. This will register a Named `HttpClient` as a Keyed service, using the client's name as a service key — and enable you to leverage the Keyed Services APIs (e.g. [`[FromKeyedServices(...)]`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute)) to obtain the required `HttpClient` instances. By default, the clients are registered with Scoped lifetime. +As of .NET 9, you need to _opt in_ to the feature by calling the extension method. If opted in, the Named client applying the configuraion is added to the DI container as a Keyed `HttpClient` service, using the client's name as a service key. With that, you can use the standard Keyed Services APIs (e.g. ) to obtain the desired Named `HttpClient` instances (created and configured by `IHttpClientFactory`). By default, the clients are registered with _Scoped_ lifetime. -Below is a full runnable example of the integration between `HttpClientFactory`, Keyed DI and ASP.NET Core 9.0 Minimal APIs: +The code below illustrates the integration between `HttpClientFactory`, Keyed DI and ASP.NET Core 9.0 Minimal APIs: :::code source="snippets/http/keyedservices/Program.cs" highlight="4,10,16"::: @@ -35,11 +35,11 @@ Endpoint response: {"name":"runtime","url":"https://api.github.com/repos/dotnet/runtime"} ``` -In the example above, the configured `HttpClient` is directly injected into the request handler via the Keyed DI infra and ASP.NET Core parameter binding. For more information on Keyed Services in ASP.NET Core, see [Dependency injection in ASP.NET Core](/aspnet/core/fundamentals/dependency-injection#keyed-services) +In the example above, the configured `HttpClient` is injected into the request handler through the standard Keyed DI infra, integrated into ASP.NET Core parameter binding. For more information on Keyed Services in ASP.NET Core, see [Dependency injection in ASP.NET Core](/aspnet/core/fundamentals/dependency-injection#keyed-services). ## Approach Comparison -Stripping away the code unrelated to `IHttpClientFactory`, you can compare the _Keyed DI approach_ +Taking only the `IHttpClientFactory` related code from the [Basic Usage](#basic-usage) example: ```csharp services.AddHttpClient("github", /* ... */).AddAsKeyed(); // (1) @@ -48,7 +48,11 @@ app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) => // (2) //httpClient.Get.... // (3) ``` -with how the same is achieved the two "older" ones: first with the _Named approach_ +The code snippet above is illustrating how the registration `(1)`, obtaining the configured `HttpClient` instance `(2)`, and using the obtained client instance as needed `(3)` can look like in the _Keyed DI approach_. + +Now we can compare how the same steps are achieved with the two "older" approaches. + +First, with the _Named approach_: ```csharp services.AddHttpClient("github", /* ... */); // (1) @@ -60,7 +64,7 @@ app.MapGet("/github", (IHttpClientFactory httpClientFactory) => }); ``` -and second, the _Typed approach_. +And second, with the _Typed approach_: ```csharp services.AddHttpClient(/* ... */); // (1) @@ -122,20 +126,23 @@ services.AddHttpClient("singleton") .AddAsKeyed(ServiceLifetime.Singleton); ``` -### ⚠️ Avoid Transient HttpClient Memory Leak ⚠️ - -We strongly recommend _avoiding_ Transient lifetime for Keyed `HttpClient`s. +### Avoid Transient HttpClient Memory Leak -Registering the client as a Keyed Transient service will lead to the `HttpClient` and `HttpMessageHandler` instances being _captured by DI container_, as both implement `IDisposable`. This can result in _memory leaks_ if the client is resolved multiple times within Singleton services. - -### ⚠️ Avoid Captive Dependency ⚠️ +> [!IMPORTANT] +> We strongly recommend _avoiding_ Transient lifetime for Keyed `HttpClient`s. +> +> Registering the client as a Keyed Transient service will lead to the `HttpClient` and `HttpMessageHandler` instances being _captured by DI container_, as both implement `IDisposable`. This can result in _memory leaks_ if the client is resolved multiple times within Singleton services. -If `HttpClient` is registered either: - - as a Keyed _Singleton_, -OR- - - as a Keyed _Scoped_ or _Transient_, and injected within a _long-running_ (longer than `HandlerLifetime`) application Scope, -OR- - - as a Keyed _Transient_, and injected into a _Singleton_ service, +### Avoid Captive Dependency -the `HttpClient` instance will become _captive_, and will most possibly outlive way beyond its expected `HandlerLifetime`. `HttpClientFactory` has no control over captive clients, they are NOT able to participate in the handler rotation, and this can result in [the loss of DNS changes](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-dns-changes). A similar issue [already exists](httpclient-factory.md#avoid-typed-clients-in-singleton-services) for Typed clients, which are registered as Transient services. +> [!IMPORTANT] +> If `HttpClient` is registered either: +> +> - as a Keyed _Singleton_, -OR- +> - as a Keyed _Scoped_ or _Transient_, and injected within a _long-running_ (longer than `HandlerLifetime`) application Scope, -OR- +> - as a Keyed _Transient_, and injected into a _Singleton_ service, +> +> the `HttpClient` instance will become _captive_, and will most possibly outlive way beyond its expected `HandlerLifetime`. `HttpClientFactory` has no control over captive clients, they are NOT able to participate in the handler rotation, and this can result in [the loss of DNS changes](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-dns-changes). A similar issue [already exists](httpclient-factory.md#avoid-typed-clients-in-singleton-services) for Typed clients, which are registered as Transient services. In cases when client's longevity cannot be avoided — or if it is concsiously desired, e.g. for a Keyed Singleton — it is advised to [leverage `SocketsHttpHandler`](httpclient-factory.md#using-ihttpclientfactory-together-with-socketshttphandler) by setting `PooledConnectionLifetime` to a reasonable value. @@ -149,13 +156,15 @@ services.AddSingleton(); public class MySingleton([FromKeyedServices("shared")] HttpClient shared) // { ... ``` -### ⚠️ Beware Of Scope Mismatch ⚠️ +### Beware Of Scope Mismatch While Scoped lifetime is much less problematic for the Named `HttpClient`s (compared to Singleton and Transient pitfalls), it has its own catch. -Keyed Scoped lifetime of a specific `HttpClient` instance will be bound — as expected — to the "ordinary" application scope (e.g. incoming request scope) where it was resolved from. However, it does NOT apply to the underlying message handler chain, which is still managed by the `HttpClientFactory`, in the same way it is for the Named clients created directly from factory. `HttpClient`s with the _same_ name, but resolved (within a `HandlerLifetime` timeframe) in two different scopes (e.g. two concurrent requests to the same endpoint), can reuse the _same_ `HttpMessageHandler` instance. Which in turn will have it's own separate scope, as illustrated in the [docs](httpclient-factory.md#message-handler-scopes-in-ihttpclientfactory). +> [!IMPORTANT] +> Keyed Scoped lifetime of a specific `HttpClient` instance will be bound — as expected — to the "ordinary" application scope (e.g. incoming request scope) where it was resolved from. However, it does NOT apply to the underlying message handler chain, which is still managed by the `HttpClientFactory`, in the same way it is for the Named clients created directly from factory. `HttpClient`s with the _same_ name, but resolved (within a `HandlerLifetime` timeframe) in two different scopes (e.g. two concurrent requests to the same endpoint), can reuse the _same_ `HttpMessageHandler` instance. Which in turn will have it's own separate scope, as illustrated in the [docs](httpclient-factory.md#message-handler-scopes-in-ihttpclientfactory). -The [Scope Mismatch](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-scoped-lifetime) problem is nasty and long-existing one, and as of .NET 9 (package-provided) is still remains [unsolved](https://github.com/dotnet/runtime/issues/47091). From a service injected through the regular DI infra, you would expect all the dependencies to be satisfied from the same scope — but be aware that for the Keyed Scoped `HttpClient`s it is, unfortunately, not the case. +> [!NOTE] +> The [Scope Mismatch](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-scoped-lifetime) problem is nasty and long-existing one, and as of .NET 9 is still remains [unsolved](https://github.com/dotnet/runtime/issues/47091). From a service injected through the regular DI infra, you would expect all the dependencies to be satisfied from the same scope — but be aware that for the Keyed Scoped `HttpClient`s it is, unfortunately, not the case. ## Keyed Message Handler Chain @@ -203,7 +212,7 @@ Technically, the example "unwraps" the Typed client, so that the previously "hid ## HOW-TO: Opt-In To Keyed DI By Default -You don't have to call `AddAsKeyed` for every single client — you can easily opt in "globally" (for any client name) via `ConfigureHttpClientDefaults`. From Keyed Services perspective, it will result in the [`KeyedService.AnyKey`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.keyedservice.anykey) registration. +You don't have to call `AddAsKeyed` for every single client — you can easily opt in "globally" (for any client name) via `ConfigureHttpClientDefaults`. From Keyed Services perspective, it will result in the registration. ```csharp services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); @@ -219,9 +228,10 @@ public class FooBarBazController( //{ ... ``` -### ⚠️ Beware Of "Unknown" Clients ⚠️ +### Beware Of "Unknown" Clients -`KeyedService.AnyKey` registrations define a mapping from _any_ key value to some service instance. However, as a result, the Container validation will not apply, and an _erroneous_ key value will _silently_ lead to a _wrong instance_ being injected. +> [!NOTE] +> `KeyedService.AnyKey` registrations define a mapping from _any_ key value to some service instance. However, as a result, the Container validation will not apply, and an _erroneous_ key value will _silently_ lead to a _wrong instance_ being injected. > [!IMPORTANT] > In case of Keyed `HttpClient`s, a mistake in the client name can result in erroneously injecting an "unknown" client — meaning, a client which name was never registered. @@ -245,7 +255,7 @@ Even though the "global" opt-in is a one-liner, it is unfortunate that the featu ## HOW-TO: Opt-Out From Keyed Registration -You can explicitly opt out from Keyed DI for `HttpClient`s by calling [`RemoveAsKeyed()`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.removeaskeyed), either per client name: +You can explicitly opt out from Keyed DI for `HttpClient`s by calling the extension method, either per client name: ```csharp services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); // opt IN by default @@ -283,10 +293,11 @@ Note that if `AddAsKeyed()` is called within a Typed client registration, only t ## See also - [IHttpClientFactory with .NET][hcf] -- [Common `IHttpClientFactory` usage issues][hcf-troubleshooting] +- [Dependency injection in .NET][di] - +- [Common `IHttpClientFactory` usage issues][hcf-troubleshooting] [hcf]: httpclient-factory.md +[di]: dependency-injection.md [hcf-troubleshooting]: httpclient-factory-troubleshooting.md -[httpclient]: ../../fundamentals/networking/http/httpclient.md diff --git a/docs/fundamentals/toc.yml b/docs/fundamentals/toc.yml index f3c21c755d52a..e5f8b50e1e579 100644 --- a/docs/fundamentals/toc.yml +++ b/docs/fundamentals/toc.yml @@ -1098,6 +1098,9 @@ items: - name: HTTP client factory troubleshooting href: ../core/extensions/httpclient-factory-troubleshooting.md displayName: httpclient,http,client,factory,named client,named httpclient,typed client,typed httpclient + - name: Keyed DI Support in HTTP client factory + href: ../core/extensions/httpclient-factory-keyed-di.md + displayName: httpclient,http,client,factory,dependency injection,keyed,keyed di,keyed services - name: Build resilient HTTP apps href: ../core/resilience/http-resilience.md displayName: resilience,transient fault handling,http,httpclient,recovery,polly From d5b7ef2e467445037d8eb5e3930bb67339fa459c Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Tue, 28 Jan 2025 11:39:54 +0000 Subject: [PATCH 4/9] fixes --- .../extensions/httpclient-factory-keyed-di.md | 77 +++++++++---------- docs/core/extensions/httpclient-factory.md | 10 +-- 2 files changed, 42 insertions(+), 45 deletions(-) diff --git a/docs/core/extensions/httpclient-factory-keyed-di.md b/docs/core/extensions/httpclient-factory-keyed-di.md index 4e814ad50a13a..e34b2433ad8b9 100644 --- a/docs/core/extensions/httpclient-factory-keyed-di.md +++ b/docs/core/extensions/httpclient-factory-keyed-di.md @@ -8,7 +8,7 @@ ms.date: 01/27/2025 # Keyed DI Support in `IHttpClientFactory` -In this article, you'll learn how to integrate `IHttpClientFactory` with Keyed Services. +In this article, you learn how to integrate `IHttpClientFactory` with Keyed Services. [_Keyed Services_](dependency-injection.md#keyed-services) (also called _Keyed DI_) is a DI feature that allows you to conveniently operate with multiple implementations of a single service. Upon registration, you can associate different _service keys_ with the specific implementations. In runtime, this key is used in lookup in combination with a service type, which means you can retrieve a specific implementation by passing the matching key. For more information on Keyed Services, and DI in general, see [.NET dependency injection][di]. @@ -16,15 +16,15 @@ For an overview on how to use `IHttpClientFactory` in your .NET application, see ## Background -`IHttpClientFactory` and Named `HttpClient` instances, unsurprisingly, align well with the Keyed Services idea. Historically, among other things, `IHttpClientFactory` was a way to overcome this long-missing DI feature. But plain Named clients require you to obtain, store and query the `IHttpClientFactory` instance — instead of injecting a configured `HttpClient` — which might be inconvenient. While Typed clients attempt to simplify that part, it comes with a catch: Typed clients are quite easy to [misconfigure](httpclient-factory-troubleshooting.md#typed-client-has-the-wrong-httpclient-injected) and [misuse](httpclient-factory.md#avoid-typed-clients-in-singleton-services), and the supporting infra can also be a tangible overhead in certain scenarios (for example, om mobile platforms). +`IHttpClientFactory` and Named `HttpClient` instances, unsurprisingly, align well with the Keyed Services idea. Historically, among other things, `IHttpClientFactory` was a way to overcome this long-missing DI feature. But plain Named clients require you to obtain, store and query the `IHttpClientFactory` instance—instead of injecting a configured `HttpClient`—which might be inconvenient. While Typed clients attempt to simplify that part, it comes with a catch: Typed clients are easy to [misconfigure](httpclient-factory-troubleshooting.md#typed-client-has-the-wrong-httpclient-injected) and [misuse](httpclient-factory.md#avoid-typed-clients-in-singleton-services), and the supporting infra can also be a tangible overhead in certain scenarios (for example, on mobile platforms). Starting from .NET 9 (`Microsoft.Extensions.Http` and `Microsoft.Extensions.DependencyInjection` packages version `9.0.0+`), `IHttpClientFactory` can leverage Keyed DI directly, introducing a new "Keyed DI approach" (as opposed to "Named" and "Typed" approaches). "Keyed DI approach" pairs the convenient, highly configurable `HttpClient` registrations with the straightforward injection of the specific configured `HttpClient` instances. ## Basic Usage -As of .NET 9, you need to _opt in_ to the feature by calling the extension method. If opted in, the Named client applying the configuraion is added to the DI container as a Keyed `HttpClient` service, using the client's name as a service key. With that, you can use the standard Keyed Services APIs (e.g. ) to obtain the desired Named `HttpClient` instances (created and configured by `IHttpClientFactory`). By default, the clients are registered with _Scoped_ lifetime. +As of .NET 9, you need to _opt in_ to the feature by calling the extension method. If opted in, the Named client applying the configuration is added to the DI container as a Keyed `HttpClient` service, using the client's name as a service key, so you can use the standard Keyed Services APIs (for example, ) to obtain the desired Named `HttpClient` instances (created and configured by `IHttpClientFactory`). By default, the clients are registered with _Scoped_ lifetime. -The code below illustrates the integration between `HttpClientFactory`, Keyed DI and ASP.NET Core 9.0 Minimal APIs: +The following code illustrates the integration between `HttpClientFactory`, Keyed DI and ASP.NET Core 9.0 Minimal APIs: :::code source="snippets/http/keyedservices/Program.cs" highlight="4,10,16"::: @@ -35,7 +35,7 @@ Endpoint response: {"name":"runtime","url":"https://api.github.com/repos/dotnet/runtime"} ``` -In the example above, the configured `HttpClient` is injected into the request handler through the standard Keyed DI infra, integrated into ASP.NET Core parameter binding. For more information on Keyed Services in ASP.NET Core, see [Dependency injection in ASP.NET Core](/aspnet/core/fundamentals/dependency-injection#keyed-services). +In the example, the configured `HttpClient` is injected into the request handler through the standard Keyed DI infra, integrated into ASP.NET Core parameter binding. For more information on Keyed Services in ASP.NET Core, see [Dependency injection in ASP.NET Core](/aspnet/core/fundamentals/dependency-injection#keyed-services). ## Approach Comparison @@ -48,7 +48,7 @@ app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) => // (2) //httpClient.Get.... // (3) ``` -The code snippet above is illustrating how the registration `(1)`, obtaining the configured `HttpClient` instance `(2)`, and using the obtained client instance as needed `(3)` can look like in the _Keyed DI approach_. +—the code snippet is illustrating how the registration `(1)`, obtaining the configured `HttpClient` instance `(2)`, and using the obtained client instance as needed `(3)` can look like in the _Keyed DI approach_. Now we can compare how the same steps are achieved with the two "older" approaches. @@ -64,7 +64,7 @@ app.MapGet("/github", (IHttpClientFactory httpClientFactory) => }); ``` -And second, with the _Typed approach_: +—and second, with the _Typed approach_: ```csharp services.AddHttpClient(/* ... */); // (1) @@ -85,7 +85,7 @@ Out of the three, the Keyed DI approach offers the most succinct way to achieve ## Built-In DI Container Validation -If you enabled the Keyed registration for a specific Named client, you can access it with any existing Keyed DI APIs. But if you erroneously try to use a name which was not enabled yet, you will get the standard Keyed DI exception: +If you enabled the Keyed registration for a specific Named client, you can access it with any existing Keyed DI APIs. But if you erroneously try to use a name which wasn't enabled yet, you get the standard Keyed DI exception: ```c# services.AddHttpClient("keyed").AddAsKeyed(); @@ -97,7 +97,7 @@ provider.GetRequiredKeyedService("keyed"); // OK provider.GetRequiredKeyedService("not-keyed"); ``` -Additionaly, the Scoped lifetime of the clients can help catching cases of captive dependencies: +Additionally, the Scoped lifetime of the clients can help catching cases of captive dependencies: ```csharp services.AddHttpClient("scoped").AddAsKeyed(); @@ -126,25 +126,27 @@ services.AddHttpClient("singleton") .AddAsKeyed(ServiceLifetime.Singleton); ``` +If you call `AddAsKeyed()` within a Typed client registration, only the underlying Named client is registered as Keyed. The Typed client itself continues to be registered as a plain Transient service. + ### Avoid Transient HttpClient Memory Leak > [!IMPORTANT] -> We strongly recommend _avoiding_ Transient lifetime for Keyed `HttpClient`s. +> `HttpClient` is `IDisposable`, so we strongly recommend _avoiding_ Transient lifetime for Keyed `HttpClient`s. > -> Registering the client as a Keyed Transient service will lead to the `HttpClient` and `HttpMessageHandler` instances being _captured by DI container_, as both implement `IDisposable`. This can result in _memory leaks_ if the client is resolved multiple times within Singleton services. +> Registering the client as a Keyed Transient service leads to the `HttpClient` and `HttpMessageHandler` instances being _captured by DI container_, as both implement `IDisposable`. This can result in _memory leaks_ if the client is resolved multiple times within Singleton services. ### Avoid Captive Dependency > [!IMPORTANT] > If `HttpClient` is registered either: > -> - as a Keyed _Singleton_, -OR- -> - as a Keyed _Scoped_ or _Transient_, and injected within a _long-running_ (longer than `HandlerLifetime`) application Scope, -OR- -> - as a Keyed _Transient_, and injected into a _Singleton_ service, +> - as a Keyed _Singleton_, -OR- +> - as a Keyed _Scoped_ or _Transient_, and injected within a _long-running_ (longer than `HandlerLifetime`) application Scope, -OR- +> - as a Keyed _Transient_, and injected into a _Singleton_ service, > -> the `HttpClient` instance will become _captive_, and will most possibly outlive way beyond its expected `HandlerLifetime`. `HttpClientFactory` has no control over captive clients, they are NOT able to participate in the handler rotation, and this can result in [the loss of DNS changes](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-dns-changes). A similar issue [already exists](httpclient-factory.md#avoid-typed-clients-in-singleton-services) for Typed clients, which are registered as Transient services. +> —the `HttpClient` instance becomes _captive_, and will most possibly outlive way beyond its expected `HandlerLifetime`. `HttpClientFactory` has no control over captive clients, they're NOT able to participate in the handler rotation, and it can result in [the loss of DNS changes](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-dns-changes). A similar issue [already exists](httpclient-factory.md#avoid-typed-clients-in-singleton-services) for Typed clients, which are registered as Transient services. -In cases when client's longevity cannot be avoided — or if it is concsiously desired, e.g. for a Keyed Singleton — it is advised to [leverage `SocketsHttpHandler`](httpclient-factory.md#using-ihttpclientfactory-together-with-socketshttphandler) by setting `PooledConnectionLifetime` to a reasonable value. +In cases when client's longevity can't be avoided—or if it's consciously desired, for example, for a Keyed Singleton—it's advised to [leverage `SocketsHttpHandler`](httpclient-factory.md#using-ihttpclientfactory-together-with-socketshttphandler) by setting `PooledConnectionLifetime` to a reasonable value. ```csharp services.AddHttpClient("shared") @@ -161,14 +163,14 @@ public class MySingleton([FromKeyedServices("shared")] HttpClient shared) // { . While Scoped lifetime is much less problematic for the Named `HttpClient`s (compared to Singleton and Transient pitfalls), it has its own catch. > [!IMPORTANT] -> Keyed Scoped lifetime of a specific `HttpClient` instance will be bound — as expected — to the "ordinary" application scope (e.g. incoming request scope) where it was resolved from. However, it does NOT apply to the underlying message handler chain, which is still managed by the `HttpClientFactory`, in the same way it is for the Named clients created directly from factory. `HttpClient`s with the _same_ name, but resolved (within a `HandlerLifetime` timeframe) in two different scopes (e.g. two concurrent requests to the same endpoint), can reuse the _same_ `HttpMessageHandler` instance. Which in turn will have it's own separate scope, as illustrated in the [docs](httpclient-factory.md#message-handler-scopes-in-ihttpclientfactory). +> Keyed Scoped lifetime of a specific `HttpClient` instance is bound—as expected—to the "ordinary" application scope (for example, incoming request scope) where it was resolved from. However, it does NOT apply to the underlying message handler chain, which is still managed by the `HttpClientFactory`, in the same way it is for the Named clients created directly from factory. `HttpClient`s with the _same_ name, but resolved (within a `HandlerLifetime` timeframe) in two different scopes (for example, two concurrent requests to the same endpoint), can reuse the _same_ `HttpMessageHandler` instance. Which in turn has its own separate scope, as illustrated in the [docs](httpclient-factory.md#message-handler-scopes-in-ihttpclientfactory). > [!NOTE] -> The [Scope Mismatch](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-scoped-lifetime) problem is nasty and long-existing one, and as of .NET 9 is still remains [unsolved](https://github.com/dotnet/runtime/issues/47091). From a service injected through the regular DI infra, you would expect all the dependencies to be satisfied from the same scope — but be aware that for the Keyed Scoped `HttpClient`s it is, unfortunately, not the case. +> The [Scope Mismatch](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-scoped-lifetime) problem is nasty and long-existing one, and as of .NET 9 still remains [unsolved](https://github.com/dotnet/runtime/issues/47091). From a service injected through the regular DI infra, you would expect all the dependencies to be satisfied from the same scope—but for the Keyed Scoped `HttpClient`s it is, unfortunately, not the case. ## Keyed Message Handler Chain -For some advanced scenarios, you might want to access `HttpMessageHandler` chain directly, instead of an `HttpClient` object. `HttpClientFactory` provides `IHttpMessageHandlerFactory` interface for that; and if you enable Keyed DI, then not only `HttpClient`, but also the respective `HttpMessageHandler` chain will be registered as a Keyed service: +For some advanced scenarios, you might want to access `HttpMessageHandler` chain directly, instead of an `HttpClient` object. `HttpClientFactory` provides `IHttpMessageHandlerFactory` interface to create the handlers; and if you enable Keyed DI, then not only `HttpClient`, but also the respective `HttpMessageHandler` chain is registered as a Keyed service: ```csharp services.AddHttpClient("foo").AddAsKeyed(); @@ -200,19 +202,20 @@ A minimal-change switch from an existing Typed client to a Keyed dependency can HttpClient httpClient) // { ... ``` -In the example above: +In the example: + 1. The registration of the Typed client `Foo` is split into: - - A registration of a Named client `nameof(Foo)` with the same `HttpClient` configuration, and opt-in to Keyed DI; and + - A registration of a Named client `nameof(Foo)` with the same `HttpClient` configuration, and an opt-in to Keyed DI; and - Plain Transient service `Foo`. 2. `HttpClient` dependency in `Foo` is explicitly bound to a Keyed Service with a key `nameof(Foo)`. -The name doesn't have to be `nameof(Foo)`, but the example aimed to minimize the behavioral changes. Typed clients use Named clients "under the hood", and by default, such "hidden" Named clients go by the linked Typed client's type name. In this case, the "hidden" name was `nameof(Foo)`, so the example preserved it. +The name doesn't have to be `nameof(Foo)`, but the example aimed to minimize the behavioral changes. Typed clients use Named clients "under the hood," and by default, such "hidden" Named clients go by the linked Typed client's type name. In this case, the "hidden" name was `nameof(Foo)`, so the example preserved it. -Technically, the example "unwraps" the Typed client, so that the previously "hidden" Named client becomes "exposed", and the dependency is satisfied via the Keyed DI infra instead of the Typed client infra. +Technically, the example "unwraps" the Typed client, so that the previously "hidden" Named client becomes "exposed," and the dependency is satisfied via the Keyed DI infra instead of the Typed client infra. ## HOW-TO: Opt-In To Keyed DI By Default -You don't have to call `AddAsKeyed` for every single client — you can easily opt in "globally" (for any client name) via `ConfigureHttpClientDefaults`. From Keyed Services perspective, it will result in the registration. +You don't have to call `AddAsKeyed` for every single client—you can easily opt in "globally" (for any client name) via `ConfigureHttpClientDefaults`. From Keyed Services perspective, it results in the registration. ```csharp services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); @@ -231,15 +234,15 @@ public class FooBarBazController( ### Beware Of "Unknown" Clients > [!NOTE] -> `KeyedService.AnyKey` registrations define a mapping from _any_ key value to some service instance. However, as a result, the Container validation will not apply, and an _erroneous_ key value will _silently_ lead to a _wrong instance_ being injected. +> `KeyedService.AnyKey` registrations define a mapping from _any_ key value to some service instance. However, as a result, the Container validation doesn't apply, and an _erroneous_ key value _silently_ leads to a _wrong instance_ being injected. > [!IMPORTANT] -> In case of Keyed `HttpClient`s, a mistake in the client name can result in erroneously injecting an "unknown" client — meaning, a client which name was never registered. +> For Keyed `HttpClient`s, a mistake in the client name can result in erroneously injecting an "unknown" client—meaning, a client which name was never registered. -Actually, in the first place, same is true for the plain Named clients: `IHttpClientFactory` doesn't require the client name to be explicitly registered (aligning with the way the [Options pattern](options.md) work). The factory will give you an unconfigured — or, more precisely, default-configured — `HttpClient` for any unknown name. +Actually, in the first place, same is true for the plain Named clients: `IHttpClientFactory` doesn't require the client name to be explicitly registered (aligning with the way the [Options pattern](options.md) work). The factory gives you an unconfigured—or, more precisely, default-configured—`HttpClient` for any unknown name. > [!NOTE] -> Therefore, it is important to keep in mind: the "Keyed by default" approach covers not only all _registered_ `HttpClient`s, but all the clients that `HttpClientFactory` is _able to create_. +> Therefore, it's important to keep in mind: the "Keyed by default" approach covers not only all _registered_ `HttpClient`s, but all the clients that `HttpClientFactory` is _able to create_. ```csharp services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); @@ -251,11 +254,11 @@ provider.GetRequiredKeyedService("unknown"); // OK (unconfigured ins ### "Opt-In" Strategy Considerations -Even though the "global" opt-in is a one-liner, it is unfortunate that the feature still requires it, instead of just working "out of the box". For full context and reasoning on that decision, see [dotnet/runtime#89755](https://github.com/dotnet/runtime/issues/89755) and [dotnet/runtime#104943](https://github.com/dotnet/runtime/pull/104943). In short, the main blocker for "on by default" is the `ServiceLifetime` "controversy": for the current (`9.0.0`) state of the DI and `HttpClientFactory` implementations, there is no single `ServiceLifetime` that would be reasonably safe for all `HttpClient`s in all possible situations. There is an intention, however, to address the caveats in the upcoming releases, and switch the strategy from "opt-in" to "opt-out". +Even though the "global" opt-in is a one-liner, it's unfortunate that the feature still requires it, instead of just working "out of the box." For full context and reasoning on that decision, see [dotnet/runtime#89755](https://github.com/dotnet/runtime/issues/89755) and [dotnet/runtime#104943](https://github.com/dotnet/runtime/pull/104943). In short, the main blocker for "on by default" is the `ServiceLifetime` "controversy": for the current (`9.0.0`) state of the DI and `HttpClientFactory` implementations, there's no single `ServiceLifetime` that would be reasonably safe for all `HttpClient`s in all possible situations. There's an intention, however, to address the caveats in the upcoming releases, and switch the strategy from "opt-in" to "opt-out". ## HOW-TO: Opt-Out From Keyed Registration -You can explicitly opt out from Keyed DI for `HttpClient`s by calling the extension method, either per client name: +You can explicitly opt out from Keyed DI for `HttpClient`s by calling the extension method, either per client name: ```csharp services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); // opt IN by default @@ -267,7 +270,7 @@ provider.GetRequiredKeyedService("not-keyed"); // Throws: No service provider.GetRequiredKeyedService("unknown"); // OK (unconfigured instance) ``` -or "globally" with `ConfigureHttpClientDefaults`: +—or "globally" with `ConfigureHttpClientDefaults`: ```csharp services.ConfigureHttpClientDefaults(b => b.RemoveAsKeyed()); // opt OUT by default @@ -279,16 +282,13 @@ provider.GetRequiredKeyedService("not-keyed"); // Throws: No service provider.GetRequiredKeyedService("unknown"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered. ``` - ## Order Of Precedence -If called together or any of them more then once, `AddAsKeyed()` and `RemoveAsKeyed()` generally follow the rules of `HttpClientFactory` configs and DI registrations: +If called together or any of them more than once, `AddAsKeyed()` and `RemoveAsKeyed()` generally follow the rules of `HttpClientFactory` configs and DI registrations: -1. If called for the same name, the last one wins: the lifetime from the last `AddAsKeyed()` is used to create the Keyed registration (unless `RemoveAsKeyed()` was called last, in which case the name is excluded). -2. If used only within `ConfigureHttpClientDefaults`, the last one wins. -3. If both `ConfigureHttpClientDefaults` and specific client name were used, all defaults are considered to "happen" before all per-name ones. Thus, defaults can be disregarded, and the last of the per-name ones wins. - -Note that if `AddAsKeyed()` is called within a Typed client registration, only the underlying Named client will be registered as Keyed. The Typed client itself will continue to be registered as a plain Transient service. +1. If called for the same name, the last setting wins: the lifetime from the last `AddAsKeyed()` is used to create the Keyed registration (unless `RemoveAsKeyed()` was called last, in which case the name is excluded). +2. If used only within `ConfigureHttpClientDefaults`, the last setting wins. +3. If both `ConfigureHttpClientDefaults` and specific client name were used, all defaults are considered to "happen" before all per-name settings. Thus, defaults can be disregarded, and the last of the per-name settings wins. ## See also @@ -300,4 +300,3 @@ Note that if `AddAsKeyed()` is called within a Typed client registration, only t [hcf]: httpclient-factory.md [di]: dependency-injection.md [hcf-troubleshooting]: httpclient-factory-troubleshooting.md - diff --git a/docs/core/extensions/httpclient-factory.md b/docs/core/extensions/httpclient-factory.md index 7d5ff7cfb52e2..dc876c87356b3 100644 --- a/docs/core/extensions/httpclient-factory.md +++ b/docs/core/extensions/httpclient-factory.md @@ -295,17 +295,15 @@ For more information, see the [full example](https://github.com/dotnet/docs/tree ## Avoid depending on "factory-default" Primary Handler -In this section, we use the term _"factory-default" Primary Handler_, by which we mean the Primary Handler that the default `IHttpClientFactory` implementation (or more precisely, the default `HttpMessageHandlerBuilder` implementation) will assign if _not configured in any way_ whatsoever. +In this section, we use the term _"factory-default" Primary Handler_, by which we mean the Primary Handler that the default `IHttpClientFactory` implementation (or more precisely, the default `HttpMessageHandlerBuilder` implementation) assigns if _not configured in any way_ whatsoever. > [!NOTE] > The "factory-default" Primary Handler is an _implementation detail_ and subject to change. -> It is strongly advised to AVOID depending on a specific implementation being used as a "factory-default" (e.g. `HttpClientHandler`). +> ❌ AVOID depending on a specific implementation being used as a "factory-default" (for example, `HttpClientHandler`). -There are cases in which you need to depended on the specific type of a Primary Handler, especially if working on a class library. While preserving the end-user's configuration, you might want to update, for example, `HttpClientHandler`-specific properies like `ClientCertificates`, `UseCookies`, `UseProxy`, etc. +There are cases in which you need to know the specific type of a Primary Handler, especially if working on a class library. While preserving the end-user's configuration, you might want to update, for example, `HttpClientHandler`-specific properties like `ClientCertificates`, `UseCookies`, `UseProxy`, etc. It might be tempting to cast the Primary handler to `HttpClientHandler`, which _happened to_ work while `HttpClientHandler` was used as the "factory-default" Primary Handler. But as any code depending on implementation details, such a workaround is _fragile_ and bound to break. -It might be tempting to simply cast the Primary handler to `HttpClientHandler`, which _happened to_ work while `HttpClientHandler` was used as the "factory-default" Primary Handler. But as any code depending on implementation details, such a workaround is _fragile_ and bound to break. - -It is advised to leverage `ConfigureHttpClientDefaults` to set up an "app-level" default Primary Handler instance instead of relying on the "factory-default" one: +Instead, you can use `ConfigureHttpClientDefaults` to set up an "app-level" default Primary Handler instance instead of relying on the "factory-default" one: ```csharp // Contract with the end-user: Only HttpClientHandler is supported. From 216cf4f0015765e99fdbb9af4b05dfe2f3d4eec1 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Tue, 28 Jan 2025 11:43:25 +0000 Subject: [PATCH 5/9] lint --- docs/core/extensions/httpclient-factory-keyed-di.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/core/extensions/httpclient-factory-keyed-di.md b/docs/core/extensions/httpclient-factory-keyed-di.md index e34b2433ad8b9..ea52d165e4455 100644 --- a/docs/core/extensions/httpclient-factory-keyed-di.md +++ b/docs/core/extensions/httpclient-factory-keyed-di.md @@ -160,7 +160,7 @@ public class MySingleton([FromKeyedServices("shared")] HttpClient shared) // { . ### Beware Of Scope Mismatch -While Scoped lifetime is much less problematic for the Named `HttpClient`s (compared to Singleton and Transient pitfalls), it has its own catch. +While Scoped lifetime is much less problematic for the Named `HttpClient`s (compared to Singleton and Transient pitfalls), it has its own catch. > [!IMPORTANT] > Keyed Scoped lifetime of a specific `HttpClient` instance is bound—as expected—to the "ordinary" application scope (for example, incoming request scope) where it was resolved from. However, it does NOT apply to the underlying message handler chain, which is still managed by the `HttpClientFactory`, in the same way it is for the Named clients created directly from factory. `HttpClient`s with the _same_ name, but resolved (within a `HandlerLifetime` timeframe) in two different scopes (for example, two concurrent requests to the same endpoint), can reuse the _same_ `HttpMessageHandler` instance. Which in turn has its own separate scope, as illustrated in the [docs](httpclient-factory.md#message-handler-scopes-in-ihttpclientfactory). @@ -258,7 +258,7 @@ Even though the "global" opt-in is a one-liner, it's unfortunate that the featur ## HOW-TO: Opt-Out From Keyed Registration -You can explicitly opt out from Keyed DI for `HttpClient`s by calling the extension method, either per client name: +You can explicitly opt out from Keyed DI for `HttpClient`s by calling the extension method, either per client name: ```csharp services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); // opt IN by default From 64a2d7f9ad03c5271e86831d9443116307a446f1 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Tue, 28 Jan 2025 11:55:50 +0000 Subject: [PATCH 6/9] fix ref --- docs/core/extensions/httpclient-factory-keyed-di.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/core/extensions/httpclient-factory-keyed-di.md b/docs/core/extensions/httpclient-factory-keyed-di.md index ea52d165e4455..93cf0bc1aada3 100644 --- a/docs/core/extensions/httpclient-factory-keyed-di.md +++ b/docs/core/extensions/httpclient-factory-keyed-di.md @@ -213,7 +213,7 @@ The name doesn't have to be `nameof(Foo)`, but the example aimed to minimize the Technically, the example "unwraps" the Typed client, so that the previously "hidden" Named client becomes "exposed," and the dependency is satisfied via the Keyed DI infra instead of the Typed client infra. -## HOW-TO: Opt-In To Keyed DI By Default +## HOW-TO: Opt In To Keyed DI By Default You don't have to call `AddAsKeyed` for every single client—you can easily opt in "globally" (for any client name) via `ConfigureHttpClientDefaults`. From Keyed Services perspective, it results in the registration. @@ -256,9 +256,9 @@ provider.GetRequiredKeyedService("unknown"); // OK (unconfigured ins Even though the "global" opt-in is a one-liner, it's unfortunate that the feature still requires it, instead of just working "out of the box." For full context and reasoning on that decision, see [dotnet/runtime#89755](https://github.com/dotnet/runtime/issues/89755) and [dotnet/runtime#104943](https://github.com/dotnet/runtime/pull/104943). In short, the main blocker for "on by default" is the `ServiceLifetime` "controversy": for the current (`9.0.0`) state of the DI and `HttpClientFactory` implementations, there's no single `ServiceLifetime` that would be reasonably safe for all `HttpClient`s in all possible situations. There's an intention, however, to address the caveats in the upcoming releases, and switch the strategy from "opt-in" to "opt-out". -## HOW-TO: Opt-Out From Keyed Registration +## HOW-TO: Opt Out From Keyed Registration -You can explicitly opt out from Keyed DI for `HttpClient`s by calling the extension method, either per client name: +You can explicitly opt out from Keyed DI for `HttpClient`s by calling the extension method, either per client name: ```csharp services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); // opt IN by default From 8de4c49c207ca777163b77f38352e6d5dd570f02 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Wed, 29 Jan 2025 17:31:19 +0100 Subject: [PATCH 7/9] Apply suggestions from code review Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> --- .../extensions/httpclient-factory-keyed-di.md | 64 +++++++++---------- docs/core/extensions/httpclient-factory.md | 4 +- docs/fundamentals/toc.yml | 2 +- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/docs/core/extensions/httpclient-factory-keyed-di.md b/docs/core/extensions/httpclient-factory-keyed-di.md index 93cf0bc1aada3..2b452f183f53b 100644 --- a/docs/core/extensions/httpclient-factory-keyed-di.md +++ b/docs/core/extensions/httpclient-factory-keyed-di.md @@ -6,17 +6,17 @@ ms.author: knatalia ms.date: 01/27/2025 --- -# Keyed DI Support in `IHttpClientFactory` +# Keyed DI support in `IHttpClientFactory` In this article, you learn how to integrate `IHttpClientFactory` with Keyed Services. -[_Keyed Services_](dependency-injection.md#keyed-services) (also called _Keyed DI_) is a DI feature that allows you to conveniently operate with multiple implementations of a single service. Upon registration, you can associate different _service keys_ with the specific implementations. In runtime, this key is used in lookup in combination with a service type, which means you can retrieve a specific implementation by passing the matching key. For more information on Keyed Services, and DI in general, see [.NET dependency injection][di]. +[_Keyed Services_](dependency-injection.md#keyed-services) (also called _Keyed DI_) is a dependency injection (DI) feature that allows you to conveniently operate with multiple implementations of a single service. Upon registration, you can associate different _service keys_ with the specific implementations. At run time, this key is used in lookup in combination with a service type, which means you can retrieve a specific implementation by passing the matching key. For more information on Keyed Services, and DI in general, see [.NET dependency injection][di]. For an overview on how to use `IHttpClientFactory` in your .NET application, see [IHttpClientFactory with .NET][hcf]. ## Background -`IHttpClientFactory` and Named `HttpClient` instances, unsurprisingly, align well with the Keyed Services idea. Historically, among other things, `IHttpClientFactory` was a way to overcome this long-missing DI feature. But plain Named clients require you to obtain, store and query the `IHttpClientFactory` instance—instead of injecting a configured `HttpClient`—which might be inconvenient. While Typed clients attempt to simplify that part, it comes with a catch: Typed clients are easy to [misconfigure](httpclient-factory-troubleshooting.md#typed-client-has-the-wrong-httpclient-injected) and [misuse](httpclient-factory.md#avoid-typed-clients-in-singleton-services), and the supporting infra can also be a tangible overhead in certain scenarios (for example, on mobile platforms). +`IHttpClientFactory` and Named `HttpClient` instances, unsurprisingly, align well with the Keyed Services idea. Historically, among other things, `IHttpClientFactory` was a way to overcome this long-missing DI feature. But plain Named clients require you to obtain, store, and query the `IHttpClientFactory` instance—instead of injecting a configured `HttpClient`—which might be inconvenient. While Typed clients attempt to simplify that part, it comes with a catch: Typed clients are easy to [misconfigure](httpclient-factory-troubleshooting.md#typed-client-has-the-wrong-httpclient-injected) and [misuse](httpclient-factory.md#avoid-typed-clients-in-singleton-services), and the supporting infrastructure can also be a tangible overhead in certain scenarios (for example, on mobile platforms). Starting from .NET 9 (`Microsoft.Extensions.Http` and `Microsoft.Extensions.DependencyInjection` packages version `9.0.0+`), `IHttpClientFactory` can leverage Keyed DI directly, introducing a new "Keyed DI approach" (as opposed to "Named" and "Typed" approaches). "Keyed DI approach" pairs the convenient, highly configurable `HttpClient` registrations with the straightforward injection of the specific configured `HttpClient` instances. @@ -24,7 +24,7 @@ Starting from .NET 9 (`Microsoft.Extensions.Http` and `Microsoft.Extensions.Depe As of .NET 9, you need to _opt in_ to the feature by calling the extension method. If opted in, the Named client applying the configuration is added to the DI container as a Keyed `HttpClient` service, using the client's name as a service key, so you can use the standard Keyed Services APIs (for example, ) to obtain the desired Named `HttpClient` instances (created and configured by `IHttpClientFactory`). By default, the clients are registered with _Scoped_ lifetime. -The following code illustrates the integration between `HttpClientFactory`, Keyed DI and ASP.NET Core 9.0 Minimal APIs: +The following code illustrates the integration between `HttpClientFactory`, Keyed DI, and ASP.NET Core 9.0 Minimal APIs: :::code source="snippets/http/keyedservices/Program.cs" highlight="4,10,16"::: @@ -35,11 +35,11 @@ Endpoint response: {"name":"runtime","url":"https://api.github.com/repos/dotnet/runtime"} ``` -In the example, the configured `HttpClient` is injected into the request handler through the standard Keyed DI infra, integrated into ASP.NET Core parameter binding. For more information on Keyed Services in ASP.NET Core, see [Dependency injection in ASP.NET Core](/aspnet/core/fundamentals/dependency-injection#keyed-services). +In the example, the configured `HttpClient` is injected into the request handler through the standard Keyed DI infrastructure, which is integrated into ASP.NET Core parameter binding. For more information on Keyed Services in ASP.NET Core, see [Dependency injection in ASP.NET Core](/aspnet/core/fundamentals/dependency-injection#keyed-services). -## Approach Comparison +## Approach comparison -Taking only the `IHttpClientFactory` related code from the [Basic Usage](#basic-usage) example: +Consider only the `IHttpClientFactory`-related code from the [Basic Usage](#basic-usage) example: ```csharp services.AddHttpClient("github", /* ... */).AddAsKeyed(); // (1) @@ -48,9 +48,9 @@ app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) => // (2) //httpClient.Get.... // (3) ``` -—the code snippet is illustrating how the registration `(1)`, obtaining the configured `HttpClient` instance `(2)`, and using the obtained client instance as needed `(3)` can look like in the _Keyed DI approach_. +This code snippet illustrates how the registration `(1)`, obtaining the configured `HttpClient` instance `(2)`, and using the obtained client instance as needed `(3)` can look when using the _Keyed DI approach_. -Now we can compare how the same steps are achieved with the two "older" approaches. +Compare how the same steps are achieved with the two "older" approaches. First, with the _Named approach_: @@ -64,7 +64,7 @@ app.MapGet("/github", (IHttpClientFactory httpClientFactory) => }); ``` -—and second, with the _Typed approach_: +Second, with the _Typed approach_: ```csharp services.AddHttpClient(/* ... */); // (1) @@ -83,11 +83,11 @@ public class GitHubClient(HttpClient httpClient) // (2) Out of the three, the Keyed DI approach offers the most succinct way to achieve the same behavior. -## Built-In DI Container Validation +## Built-in DI container validation -If you enabled the Keyed registration for a specific Named client, you can access it with any existing Keyed DI APIs. But if you erroneously try to use a name which wasn't enabled yet, you get the standard Keyed DI exception: +If you enabled the Keyed registration for a specific Named client, you can access it with any existing Keyed DI APIs. But if you erroneously try to use a name that isn't enabled yet, you get the standard Keyed DI exception: -```c# +```csharp services.AddHttpClient("keyed").AddAsKeyed(); services.AddHttpClient("not-keyed"); @@ -97,7 +97,7 @@ provider.GetRequiredKeyedService("keyed"); // OK provider.GetRequiredKeyedService("not-keyed"); ``` -Additionally, the Scoped lifetime of the clients can help catching cases of captive dependencies: +Additionally, the Scoped lifetime of the clients can help catch cases of captive dependencies: ```csharp services.AddHttpClient("scoped").AddAsKeyed(); @@ -114,7 +114,7 @@ public class CapturingSingleton([FromKeyedServices("scoped")] HttpClient httpCli //{ ... ``` -## Service Lifetime Selection +## Service lifetime selection By default, `AddAsKeyed()` registers `HttpClient` as a Keyed _Scoped_ service. You can also explicitly specify the lifetime by passing the `ServiceLifetime` parameter to the `AddAsKeyed()` method: @@ -128,14 +128,14 @@ services.AddHttpClient("singleton") If you call `AddAsKeyed()` within a Typed client registration, only the underlying Named client is registered as Keyed. The Typed client itself continues to be registered as a plain Transient service. -### Avoid Transient HttpClient Memory Leak +### Avoid transient HttpClient memory leak > [!IMPORTANT] > `HttpClient` is `IDisposable`, so we strongly recommend _avoiding_ Transient lifetime for Keyed `HttpClient`s. > > Registering the client as a Keyed Transient service leads to the `HttpClient` and `HttpMessageHandler` instances being _captured by DI container_, as both implement `IDisposable`. This can result in _memory leaks_ if the client is resolved multiple times within Singleton services. -### Avoid Captive Dependency +### Avoid captive dependency > [!IMPORTANT] > If `HttpClient` is registered either: @@ -144,7 +144,7 @@ If you call `AddAsKeyed()` within a Typed client registration, only the underlyi > - as a Keyed _Scoped_ or _Transient_, and injected within a _long-running_ (longer than `HandlerLifetime`) application Scope, -OR- > - as a Keyed _Transient_, and injected into a _Singleton_ service, > -> —the `HttpClient` instance becomes _captive_, and will most possibly outlive way beyond its expected `HandlerLifetime`. `HttpClientFactory` has no control over captive clients, they're NOT able to participate in the handler rotation, and it can result in [the loss of DNS changes](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-dns-changes). A similar issue [already exists](httpclient-factory.md#avoid-typed-clients-in-singleton-services) for Typed clients, which are registered as Transient services. +> —the `HttpClient` instance becomes _captive_, and will likely outlive its expected `HandlerLifetime`. `HttpClientFactory` has no control over captive clients, they're NOT able to participate in the handler rotation, and it can result in [the loss of DNS changes](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-dns-changes). A similar issue [already exists](httpclient-factory.md#avoid-typed-clients-in-singleton-services) for Typed clients, which are registered as Transient services. In cases when client's longevity can't be avoided—or if it's consciously desired, for example, for a Keyed Singleton—it's advised to [leverage `SocketsHttpHandler`](httpclient-factory.md#using-ihttpclientfactory-together-with-socketshttphandler) by setting `PooledConnectionLifetime` to a reasonable value. @@ -158,17 +158,17 @@ services.AddSingleton(); public class MySingleton([FromKeyedServices("shared")] HttpClient shared) // { ... ``` -### Beware Of Scope Mismatch +### Beware of scope mismatch While Scoped lifetime is much less problematic for the Named `HttpClient`s (compared to Singleton and Transient pitfalls), it has its own catch. > [!IMPORTANT] -> Keyed Scoped lifetime of a specific `HttpClient` instance is bound—as expected—to the "ordinary" application scope (for example, incoming request scope) where it was resolved from. However, it does NOT apply to the underlying message handler chain, which is still managed by the `HttpClientFactory`, in the same way it is for the Named clients created directly from factory. `HttpClient`s with the _same_ name, but resolved (within a `HandlerLifetime` timeframe) in two different scopes (for example, two concurrent requests to the same endpoint), can reuse the _same_ `HttpMessageHandler` instance. Which in turn has its own separate scope, as illustrated in the [docs](httpclient-factory.md#message-handler-scopes-in-ihttpclientfactory). +> Keyed Scoped lifetime of a specific `HttpClient` instance is bound—as expected—to the "ordinary" application scope (for example, incoming request scope) where it was resolved from. However, it does NOT apply to the underlying message handler chain, which is still managed by the `HttpClientFactory`, in the same way it is for the Named clients created directly from factory. `HttpClient`s with the _same_ name, but resolved (within a `HandlerLifetime` timeframe) in two different scopes (for example, two concurrent requests to the same endpoint), can reuse the _same_ `HttpMessageHandler` instance. That instance, in turn, has its own separate scope, as illustrated in the [Message handler scopes](httpclient-factory.md#message-handler-scopes-in-ihttpclientfactory). > [!NOTE] -> The [Scope Mismatch](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-scoped-lifetime) problem is nasty and long-existing one, and as of .NET 9 still remains [unsolved](https://github.com/dotnet/runtime/issues/47091). From a service injected through the regular DI infra, you would expect all the dependencies to be satisfied from the same scope—but for the Keyed Scoped `HttpClient`s it is, unfortunately, not the case. +> The [Scope Mismatch](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-scoped-lifetime) problem is nasty and long-existing one, and as of .NET 9 still remains [unsolved](https://github.com/dotnet/runtime/issues/47091). From a service injected through the regular DI infra, you would expect all the dependencies to be satisfied from the same scope—but for the Keyed Scoped `HttpClient` instances, that's unfortunately not the case. -## Keyed Message Handler Chain +## Keyed message handler chain For some advanced scenarios, you might want to access `HttpMessageHandler` chain directly, instead of an `HttpClient` object. `HttpClientFactory` provides `IHttpMessageHandlerFactory` interface to create the handlers; and if you enable Keyed DI, then not only `HttpClient`, but also the respective `HttpMessageHandler` chain is registered as a Keyed service: @@ -179,7 +179,7 @@ var handler = provider.GetRequiredKeyedService("foo"); var invoker = new HttpMessageInvoker(handler, disposeHandler: false); ``` -## HOW-TO: Switch from Typed approach to Keyed DI +## How to: Switch from Typed approach to Keyed DI > [!NOTE] > We currently recommend using Keyed DI approach instead of Typed clients. @@ -209,11 +209,11 @@ In the example: - Plain Transient service `Foo`. 2. `HttpClient` dependency in `Foo` is explicitly bound to a Keyed Service with a key `nameof(Foo)`. -The name doesn't have to be `nameof(Foo)`, but the example aimed to minimize the behavioral changes. Typed clients use Named clients "under the hood," and by default, such "hidden" Named clients go by the linked Typed client's type name. In this case, the "hidden" name was `nameof(Foo)`, so the example preserved it. +The name doesn't have to be `nameof(Foo)`, but the example aimed to minimize the behavioral changes. Internally, typed clients use Named clients, and by default, such "hidden" Named clients go by the linked Typed client's type name. In this case, the "hidden" name was `nameof(Foo)`, so the example preserved it. Technically, the example "unwraps" the Typed client, so that the previously "hidden" Named client becomes "exposed," and the dependency is satisfied via the Keyed DI infra instead of the Typed client infra. -## HOW-TO: Opt In To Keyed DI By Default +## How to: Opt in to Keyed DI by default You don't have to call `AddAsKeyed` for every single client—you can easily opt in "globally" (for any client name) via `ConfigureHttpClientDefaults`. From Keyed Services perspective, it results in the registration. @@ -231,15 +231,15 @@ public class FooBarBazController( //{ ... ``` -### Beware Of "Unknown" Clients +### Beware Of "Unknown" clients > [!NOTE] > `KeyedService.AnyKey` registrations define a mapping from _any_ key value to some service instance. However, as a result, the Container validation doesn't apply, and an _erroneous_ key value _silently_ leads to a _wrong instance_ being injected. > [!IMPORTANT] -> For Keyed `HttpClient`s, a mistake in the client name can result in erroneously injecting an "unknown" client—meaning, a client which name was never registered. +> For Keyed `HttpClient`s, a mistake in the client name can result in erroneously injecting an "unknown" client—meaning, a client whose name was never registered. -Actually, in the first place, same is true for the plain Named clients: `IHttpClientFactory` doesn't require the client name to be explicitly registered (aligning with the way the [Options pattern](options.md) work). The factory gives you an unconfigured—or, more precisely, default-configured—`HttpClient` for any unknown name. +The same is true for the plain Named clients: `IHttpClientFactory` doesn't require the client name to be explicitly registered (aligning with the way the [Options pattern](options.md) works). The factory gives you an unconfigured—or, more precisely, default-configured—`HttpClient` for any unknown name. > [!NOTE] > Therefore, it's important to keep in mind: the "Keyed by default" approach covers not only all _registered_ `HttpClient`s, but all the clients that `HttpClientFactory` is _able to create_. @@ -252,11 +252,11 @@ provider.GetRequiredKeyedService("known"); // OK provider.GetRequiredKeyedService("unknown"); // OK (unconfigured instance) ``` -### "Opt-In" Strategy Considerations +### "Opt-in" strategy considerations Even though the "global" opt-in is a one-liner, it's unfortunate that the feature still requires it, instead of just working "out of the box." For full context and reasoning on that decision, see [dotnet/runtime#89755](https://github.com/dotnet/runtime/issues/89755) and [dotnet/runtime#104943](https://github.com/dotnet/runtime/pull/104943). In short, the main blocker for "on by default" is the `ServiceLifetime` "controversy": for the current (`9.0.0`) state of the DI and `HttpClientFactory` implementations, there's no single `ServiceLifetime` that would be reasonably safe for all `HttpClient`s in all possible situations. There's an intention, however, to address the caveats in the upcoming releases, and switch the strategy from "opt-in" to "opt-out". -## HOW-TO: Opt Out From Keyed Registration +## How to: Opt out from keyed registration You can explicitly opt out from Keyed DI for `HttpClient`s by calling the extension method, either per client name: @@ -270,7 +270,7 @@ provider.GetRequiredKeyedService("not-keyed"); // Throws: No service provider.GetRequiredKeyedService("unknown"); // OK (unconfigured instance) ``` -—or "globally" with `ConfigureHttpClientDefaults`: +Or "globally" with )>: ```csharp services.ConfigureHttpClientDefaults(b => b.RemoveAsKeyed()); // opt OUT by default @@ -282,7 +282,7 @@ provider.GetRequiredKeyedService("not-keyed"); // Throws: No service provider.GetRequiredKeyedService("unknown"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered. ``` -## Order Of Precedence +## Order of precedence If called together or any of them more than once, `AddAsKeyed()` and `RemoveAsKeyed()` generally follow the rules of `HttpClientFactory` configs and DI registrations: diff --git a/docs/core/extensions/httpclient-factory.md b/docs/core/extensions/httpclient-factory.md index dc876c87356b3..bdd4e9a0020dd 100644 --- a/docs/core/extensions/httpclient-factory.md +++ b/docs/core/extensions/httpclient-factory.md @@ -295,13 +295,13 @@ For more information, see the [full example](https://github.com/dotnet/docs/tree ## Avoid depending on "factory-default" Primary Handler -In this section, we use the term _"factory-default" Primary Handler_, by which we mean the Primary Handler that the default `IHttpClientFactory` implementation (or more precisely, the default `HttpMessageHandlerBuilder` implementation) assigns if _not configured in any way_ whatsoever. +In this section, the term _"factory-default" Primary Handler_ refers to the Primary Handler that the default `IHttpClientFactory` implementation (or more precisely, the default `HttpMessageHandlerBuilder` implementation) assigns if _not configured in any way_ whatsoever. > [!NOTE] > The "factory-default" Primary Handler is an _implementation detail_ and subject to change. > ❌ AVOID depending on a specific implementation being used as a "factory-default" (for example, `HttpClientHandler`). -There are cases in which you need to know the specific type of a Primary Handler, especially if working on a class library. While preserving the end-user's configuration, you might want to update, for example, `HttpClientHandler`-specific properties like `ClientCertificates`, `UseCookies`, `UseProxy`, etc. It might be tempting to cast the Primary handler to `HttpClientHandler`, which _happened to_ work while `HttpClientHandler` was used as the "factory-default" Primary Handler. But as any code depending on implementation details, such a workaround is _fragile_ and bound to break. +There are cases in which you need to know the specific type of a Primary Handler, especially if working on a class library. While preserving the end user's configuration, you might want to update, for example, `HttpClientHandler`-specific properties like `ClientCertificates`, `UseCookies`, and `UseProxy`. It might be tempting to cast the Primary handler to `HttpClientHandler`, which _happened to_ work while `HttpClientHandler` was used as the "factory-default" Primary Handler. But as any code depending on implementation details, such a workaround is _fragile_ and bound to break. Instead, you can use `ConfigureHttpClientDefaults` to set up an "app-level" default Primary Handler instance instead of relying on the "factory-default" one: diff --git a/docs/fundamentals/toc.yml b/docs/fundamentals/toc.yml index e5f8b50e1e579..17fbf7ace9f01 100644 --- a/docs/fundamentals/toc.yml +++ b/docs/fundamentals/toc.yml @@ -1098,7 +1098,7 @@ items: - name: HTTP client factory troubleshooting href: ../core/extensions/httpclient-factory-troubleshooting.md displayName: httpclient,http,client,factory,named client,named httpclient,typed client,typed httpclient - - name: Keyed DI Support in HTTP client factory + - name: Keyed DI support in HTTP client factory href: ../core/extensions/httpclient-factory-keyed-di.md displayName: httpclient,http,client,factory,dependency injection,keyed,keyed di,keyed services - name: Build resilient HTTP apps From 14ba341fe7fc48157aaeac4bca7fdf7e7162ad22 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Wed, 29 Jan 2025 18:21:20 +0000 Subject: [PATCH 8/9] remove foo --- .../extensions/httpclient-factory-keyed-di.md | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/core/extensions/httpclient-factory-keyed-di.md b/docs/core/extensions/httpclient-factory-keyed-di.md index 2b452f183f53b..2ed964285d6d3 100644 --- a/docs/core/extensions/httpclient-factory-keyed-di.md +++ b/docs/core/extensions/httpclient-factory-keyed-di.md @@ -24,7 +24,7 @@ Starting from .NET 9 (`Microsoft.Extensions.Http` and `Microsoft.Extensions.Depe As of .NET 9, you need to _opt in_ to the feature by calling the extension method. If opted in, the Named client applying the configuration is added to the DI container as a Keyed `HttpClient` service, using the client's name as a service key, so you can use the standard Keyed Services APIs (for example, ) to obtain the desired Named `HttpClient` instances (created and configured by `IHttpClientFactory`). By default, the clients are registered with _Scoped_ lifetime. -The following code illustrates the integration between `HttpClientFactory`, Keyed DI, and ASP.NET Core 9.0 Minimal APIs: +The following code illustrates the integration between `IHttpClientFactory`, Keyed DI, and ASP.NET Core 9.0 Minimal APIs: :::code source="snippets/http/keyedservices/Program.cs" highlight="4,10,16"::: @@ -144,7 +144,7 @@ If you call `AddAsKeyed()` within a Typed client registration, only the underlyi > - as a Keyed _Scoped_ or _Transient_, and injected within a _long-running_ (longer than `HandlerLifetime`) application Scope, -OR- > - as a Keyed _Transient_, and injected into a _Singleton_ service, > -> —the `HttpClient` instance becomes _captive_, and will likely outlive its expected `HandlerLifetime`. `HttpClientFactory` has no control over captive clients, they're NOT able to participate in the handler rotation, and it can result in [the loss of DNS changes](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-dns-changes). A similar issue [already exists](httpclient-factory.md#avoid-typed-clients-in-singleton-services) for Typed clients, which are registered as Transient services. +> —the `HttpClient` instance becomes _captive_, and will likely outlive its expected `HandlerLifetime`. `IHttpClientFactory` has no control over captive clients, they're NOT able to participate in the handler rotation, and it can result in [the loss of DNS changes](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-dns-changes). A similar issue [already exists](httpclient-factory.md#avoid-typed-clients-in-singleton-services) for Typed clients, which are registered as Transient services. In cases when client's longevity can't be avoided—or if it's consciously desired, for example, for a Keyed Singleton—it's advised to [leverage `SocketsHttpHandler`](httpclient-factory.md#using-ihttpclientfactory-together-with-socketshttphandler) by setting `PooledConnectionLifetime` to a reasonable value. @@ -163,19 +163,19 @@ public class MySingleton([FromKeyedServices("shared")] HttpClient shared) // { . While Scoped lifetime is much less problematic for the Named `HttpClient`s (compared to Singleton and Transient pitfalls), it has its own catch. > [!IMPORTANT] -> Keyed Scoped lifetime of a specific `HttpClient` instance is bound—as expected—to the "ordinary" application scope (for example, incoming request scope) where it was resolved from. However, it does NOT apply to the underlying message handler chain, which is still managed by the `HttpClientFactory`, in the same way it is for the Named clients created directly from factory. `HttpClient`s with the _same_ name, but resolved (within a `HandlerLifetime` timeframe) in two different scopes (for example, two concurrent requests to the same endpoint), can reuse the _same_ `HttpMessageHandler` instance. That instance, in turn, has its own separate scope, as illustrated in the [Message handler scopes](httpclient-factory.md#message-handler-scopes-in-ihttpclientfactory). +> Keyed Scoped lifetime of a specific `HttpClient` instance is bound—as expected—to the "ordinary" application scope (for example, incoming request scope) where it was resolved from. However, it does NOT apply to the underlying message handler chain, which is still managed by the `IHttpClientFactory`, in the same way it is for the Named clients created directly from factory. `HttpClient`s with the _same_ name, but resolved (within a `HandlerLifetime` timeframe) in two different scopes (for example, two concurrent requests to the same endpoint), can reuse the _same_ `HttpMessageHandler` instance. That instance, in turn, has its own separate scope, as illustrated in the [Message handler scopes](httpclient-factory.md#message-handler-scopes-in-ihttpclientfactory). > [!NOTE] > The [Scope Mismatch](httpclient-factory-troubleshooting.md#httpclient-doesnt-respect-scoped-lifetime) problem is nasty and long-existing one, and as of .NET 9 still remains [unsolved](https://github.com/dotnet/runtime/issues/47091). From a service injected through the regular DI infra, you would expect all the dependencies to be satisfied from the same scope—but for the Keyed Scoped `HttpClient` instances, that's unfortunately not the case. ## Keyed message handler chain -For some advanced scenarios, you might want to access `HttpMessageHandler` chain directly, instead of an `HttpClient` object. `HttpClientFactory` provides `IHttpMessageHandlerFactory` interface to create the handlers; and if you enable Keyed DI, then not only `HttpClient`, but also the respective `HttpMessageHandler` chain is registered as a Keyed service: +For some advanced scenarios, you might want to access `HttpMessageHandler` chain directly, instead of an `HttpClient` object. `IHttpClientFactory` provides `IHttpMessageHandlerFactory` interface to create the handlers; and if you enable Keyed DI, then not only `HttpClient`, but also the respective `HttpMessageHandler` chain is registered as a Keyed service: ```csharp -services.AddHttpClient("foo").AddAsKeyed(); +services.AddHttpClient("keyed-handler").AddAsKeyed(); -var handler = provider.GetRequiredKeyedService("foo"); +var handler = provider.GetRequiredKeyedService("keyed-handler"); var invoker = new HttpMessageInvoker(handler, disposeHandler: false); ``` @@ -187,47 +187,47 @@ var invoker = new HttpMessageInvoker(handler, disposeHandler: false); A minimal-change switch from an existing Typed client to a Keyed dependency can look as follows: ```diff -- services.AddHttpClient( // (1) Typed client -+ services.AddHttpClient(nameof(Foo), // (1) Named client - c => { /* ... */ } // HttpClient configuration +- services.AddHttpClient( // (1) Typed client ++ services.AddHttpClient(nameof(Service), // (1) Named client + c => { /* ... */ } // HttpClient configuration //).Configure.... - ); -+ ).AddAsKeyed(); // (1) + Keyed DI opt-in ++ ).AddAsKeyed(); // (1) + Keyed DI opt-in -+ services.AddTransient(); // (1) Plain Transient service ++ services.AddTransient(); // (1) Plain Transient service - public class Foo( -- // (2) Implicit ("hidden" Named) dependency -+ [FromKeyedServices(nameof(Foo))] // (2) Explicit Keyed Service dependency + public class Service( +- // (2) "Hidden" Named dependency ++ [FromKeyedServices(nameof(Service))] // (2) Explicit Keyed dependency HttpClient httpClient) // { ... ``` In the example: -1. The registration of the Typed client `Foo` is split into: - - A registration of a Named client `nameof(Foo)` with the same `HttpClient` configuration, and an opt-in to Keyed DI; and - - Plain Transient service `Foo`. -2. `HttpClient` dependency in `Foo` is explicitly bound to a Keyed Service with a key `nameof(Foo)`. +1. The registration of the Typed client `Service` is split into: + - A registration of a Named client `nameof(Service)` with the same `HttpClient` configuration, and an opt-in to Keyed DI; and + - Plain Transient service `Service`. +2. `HttpClient` dependency in `Service` is explicitly bound to a Keyed Service with a key `nameof(Service)`. -The name doesn't have to be `nameof(Foo)`, but the example aimed to minimize the behavioral changes. Internally, typed clients use Named clients, and by default, such "hidden" Named clients go by the linked Typed client's type name. In this case, the "hidden" name was `nameof(Foo)`, so the example preserved it. +The name doesn't have to be `nameof(Service)`, but the example aimed to minimize the behavioral changes. Internally, typed clients use Named clients, and by default, such "hidden" Named clients go by the linked Typed client's type name. In this case, the "hidden" name was `nameof(Service)`, so the example preserved it. Technically, the example "unwraps" the Typed client, so that the previously "hidden" Named client becomes "exposed," and the dependency is satisfied via the Keyed DI infra instead of the Typed client infra. ## How to: Opt in to Keyed DI by default -You don't have to call `AddAsKeyed` for every single client—you can easily opt in "globally" (for any client name) via `ConfigureHttpClientDefaults`. From Keyed Services perspective, it results in the registration. +You don't have to call `AddAsKeyed` for every single client—you can easily opt in "globally" (for any client name) via . From Keyed Services perspective, it results in the registration. ```csharp services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); -services.AddHttpClient("foo", /* ... */); -services.AddHttpClient("bar", /* ... */); -services.AddHttpClient("baz", /* ... */); +services.AddHttpClient("first", /* ... */); +services.AddHttpClient("second", /* ... */); +services.AddHttpClient("third", /* ... */); -public class FooBarBazController( - [FromKeyedServices("foo")] HttpClient foo, - [FromKeyedServices("bar")] HttpClient bar, - [FromKeyedServices("baz")] HttpClient baz) +public class MyController( + [FromKeyedServices("first")] HttpClient first, + [FromKeyedServices("second")] HttpClient second, + [FromKeyedServices("third")] HttpClient third) //{ ... ``` @@ -242,7 +242,7 @@ public class FooBarBazController( The same is true for the plain Named clients: `IHttpClientFactory` doesn't require the client name to be explicitly registered (aligning with the way the [Options pattern](options.md) works). The factory gives you an unconfigured—or, more precisely, default-configured—`HttpClient` for any unknown name. > [!NOTE] -> Therefore, it's important to keep in mind: the "Keyed by default" approach covers not only all _registered_ `HttpClient`s, but all the clients that `HttpClientFactory` is _able to create_. +> Therefore, it's important to keep in mind: the "Keyed by default" approach covers not only all _registered_ `HttpClient`s, but all the clients that `IHttpClientFactory` is _able to create_. ```csharp services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); @@ -254,7 +254,7 @@ provider.GetRequiredKeyedService("unknown"); // OK (unconfigured ins ### "Opt-in" strategy considerations -Even though the "global" opt-in is a one-liner, it's unfortunate that the feature still requires it, instead of just working "out of the box." For full context and reasoning on that decision, see [dotnet/runtime#89755](https://github.com/dotnet/runtime/issues/89755) and [dotnet/runtime#104943](https://github.com/dotnet/runtime/pull/104943). In short, the main blocker for "on by default" is the `ServiceLifetime` "controversy": for the current (`9.0.0`) state of the DI and `HttpClientFactory` implementations, there's no single `ServiceLifetime` that would be reasonably safe for all `HttpClient`s in all possible situations. There's an intention, however, to address the caveats in the upcoming releases, and switch the strategy from "opt-in" to "opt-out". +Even though the "global" opt-in is a one-liner, it's unfortunate that the feature still requires it, instead of just working "out of the box." For full context and reasoning on that decision, see [dotnet/runtime#89755](https://github.com/dotnet/runtime/issues/89755) and [dotnet/runtime#104943](https://github.com/dotnet/runtime/pull/104943). In short, the main blocker for "on by default" is the `ServiceLifetime` "controversy": for the current (`9.0.0`) state of the DI and `IHttpClientFactory` implementations, there's no single `ServiceLifetime` that would be reasonably safe for all `HttpClient`s in all possible situations. There's an intention, however, to address the caveats in the upcoming releases, and switch the strategy from "opt-in" to "opt-out". ## How to: Opt out from keyed registration @@ -270,7 +270,7 @@ provider.GetRequiredKeyedService("not-keyed"); // Throws: No service provider.GetRequiredKeyedService("unknown"); // OK (unconfigured instance) ``` -Or "globally" with )>: +Or "globally" with : ```csharp services.ConfigureHttpClientDefaults(b => b.RemoveAsKeyed()); // opt OUT by default @@ -284,7 +284,7 @@ provider.GetRequiredKeyedService("unknown"); // Throws: No service ## Order of precedence -If called together or any of them more than once, `AddAsKeyed()` and `RemoveAsKeyed()` generally follow the rules of `HttpClientFactory` configs and DI registrations: +If called together or any of them more than once, `AddAsKeyed()` and `RemoveAsKeyed()` generally follow the rules of `IHttpClientFactory` configs and DI registrations: 1. If called for the same name, the last setting wins: the lifetime from the last `AddAsKeyed()` is used to create the Keyed registration (unless `RemoveAsKeyed()` was called last, in which case the name is excluded). 2. If used only within `ConfigureHttpClientDefaults`, the last setting wins. From 973c4af7605354991543de4d49ba3a815a54ac26 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Wed, 29 Jan 2025 21:20:58 +0100 Subject: [PATCH 9/9] Apply suggestions from code review Co-authored-by: Anton Firszov --- docs/core/extensions/httpclient-factory-keyed-di.md | 6 +++--- docs/core/extensions/httpclient-factory.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/core/extensions/httpclient-factory-keyed-di.md b/docs/core/extensions/httpclient-factory-keyed-di.md index 2ed964285d6d3..eb508cc0d29ea 100644 --- a/docs/core/extensions/httpclient-factory-keyed-di.md +++ b/docs/core/extensions/httpclient-factory-keyed-di.md @@ -37,7 +37,7 @@ Endpoint response: In the example, the configured `HttpClient` is injected into the request handler through the standard Keyed DI infrastructure, which is integrated into ASP.NET Core parameter binding. For more information on Keyed Services in ASP.NET Core, see [Dependency injection in ASP.NET Core](/aspnet/core/fundamentals/dependency-injection#keyed-services). -## Approach comparison +## Comparison of Keyed, Named, and Typed approaches Consider only the `IHttpClientFactory`-related code from the [Basic Usage](#basic-usage) example: @@ -131,7 +131,7 @@ If you call `AddAsKeyed()` within a Typed client registration, only the underlyi ### Avoid transient HttpClient memory leak > [!IMPORTANT] -> `HttpClient` is `IDisposable`, so we strongly recommend _avoiding_ Transient lifetime for Keyed `HttpClient`s. +> `HttpClient` is `IDisposable`, so we strongly recommend _avoiding_ Transient lifetime for Keyed `HttpClient` instances. > > Registering the client as a Keyed Transient service leads to the `HttpClient` and `HttpMessageHandler` instances being _captured by DI container_, as both implement `IDisposable`. This can result in _memory leaks_ if the client is resolved multiple times within Singleton services. @@ -215,7 +215,7 @@ Technically, the example "unwraps" the Typed client, so that the previously "hid ## How to: Opt in to Keyed DI by default -You don't have to call `AddAsKeyed` for every single client—you can easily opt in "globally" (for any client name) via . From Keyed Services perspective, it results in the registration. +You don't have to call for every single client—you can easily opt in "globally" (for any client name) via . From Keyed Services perspective, it results in the registration. ```csharp services.ConfigureHttpClientDefaults(b => b.AddAsKeyed()); diff --git a/docs/core/extensions/httpclient-factory.md b/docs/core/extensions/httpclient-factory.md index bdd4e9a0020dd..5c67bc01c4f33 100644 --- a/docs/core/extensions/httpclient-factory.md +++ b/docs/core/extensions/httpclient-factory.md @@ -303,7 +303,7 @@ In this section, the term _"factory-default" Primary Handler_ refers to the Prim There are cases in which you need to know the specific type of a Primary Handler, especially if working on a class library. While preserving the end user's configuration, you might want to update, for example, `HttpClientHandler`-specific properties like `ClientCertificates`, `UseCookies`, and `UseProxy`. It might be tempting to cast the Primary handler to `HttpClientHandler`, which _happened to_ work while `HttpClientHandler` was used as the "factory-default" Primary Handler. But as any code depending on implementation details, such a workaround is _fragile_ and bound to break. -Instead, you can use `ConfigureHttpClientDefaults` to set up an "app-level" default Primary Handler instance instead of relying on the "factory-default" one: +Instead of relying on the "factory-default" Primary Handler, you can use `ConfigureHttpClientDefaults` to set up an "app-level" default Primary Handler instance: ```csharp // Contract with the end-user: Only HttpClientHandler is supported.