-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Integrate keyed DI with HttpClientFactory #90272
Conversation
Tagging subscribers to this area: @dotnet/ncl Issue Details
Transient lifetime was chosen to make this var client1 = services.GetRequiredKeyedService<HttpClient>(name);
var client2 = services.GetRequiredKeyedService<HttpClient>(name); // new instance functionally equivalent to this var client1 = factory.CreateClient(name);
var client2 = factory.CreateClient(name); // new instance Especially if needed to use in singletons. Scenarios made possible by (1): Constructor injection with the attribute (can substitute Typed clients): class KeyedClientTestService
{
public KeyedClientTestService([FromKeyedServices("test")] HttpClient httpClient)
{
HttpClient = httpClient;
} Resolving by name directly from service provider (needed when name is not a compile-time constant): var client = services.GetRequiredKeyedService<HttpClient>(name);
// or
var handler = services.GetRequiredKeyedService<HttpMessageHandler>(name); Scenarios made possible by (2): Registering different implementations per name serviceCollection.AddKeyedSingleton<BaseLogger, FirstClientLogger>("FirstClient");
serviceCollection.AddKeyedSingleton<BaseLogger, SecondClientLogger>("SecondClient");
serviceCollection.AddHttpClient("FirstClient")
.AddLogger<BaseLogger>(); // will use FirstClientLogger
serviceCollection.AddHttpClient("SecondClient")
.AddLogger<BaseLogger>(); // will use SecondClientLogger Easy access to client name from implementation serviceCollection.AddKeyedSingleton<KeyedHttpClientLogger>(KeyedService.AnyKey); // registered only once
serviceCollection.AddHttpClient("FirstClient")
.AddLogger<KeyedHttpClientLogger>(); // "KeyedHttpClientLogger.FirstClient" category
serviceCollection.AddHttpClient("SecondClient")
.AddLogger<KeyedHttpClientLogger>(); // "KeyedHttpClientLogger.SecondClient" category
class KeyedHttpClientLogger : IHttpClientLogger
{
private readonly ILogger _logger;
public KeyedHttpClientLogger(ILoggerFactory loggerFactory, [ServiceKey] string key)
{
_logger = loggerFactory.CreateLogger(nameof(KeyedHttpClientLogger) + "." + key);
} Fixes #89755
|
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
@halter73 @JamesNK @stephentoub could you please take a look? |
internal IReadOnlyList<Action<IPrimaryHandlerBuilder>> PrimaryHandlerActions => _primaryHandlerActions!; | ||
internal IReadOnlyList<Action<IAdditionalHandlersBuilder>> AdditionalHandlersActions => _additionalHandlersActions!; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why IReadOnlyList
rather than List
? The latter will be more efficient, especially if code ever enumerates these. And they're internal, so you don't need to worry about breaks if you ever decided to change the type.
private List<Action<IPrimaryHandlerBuilder>>? _primaryHandlerActions = new List<Action<IPrimaryHandlerBuilder>>(); | ||
private List<Action<IAdditionalHandlersBuilder>>? _additionalHandlersActions = new List<Action<IAdditionalHandlersBuilder>>(); | ||
|
||
internal IReadOnlyList<Action<IPrimaryHandlerBuilder>> PrimaryHandlerActions => _primaryHandlerActions!; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the nullable suppression? I'd have expected instead these would be nullable but then you'd annotate members like MergedToHandlerBuilderActions with [MemberNotNull(...)]
to indicate that they can be used as a guard.
for (int i = 0; i < options.PrimaryHandlerActions.Count; i++) | ||
{ | ||
options.PrimaryHandlerActions[i](b); | ||
} | ||
|
||
for (int i = 0; i < options.AdditionalHandlersActions.Count; i++) | ||
{ | ||
options.AdditionalHandlersActions[i](b); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you change PrimaryHandlerActions / AdditionalHandlersActions to be List<T>
instead of IReadOnlyList<T>
, then you can foreach
these, instead.
@@ -28,7 +28,26 @@ public DefaultHttpMessageHandlerBuilder(IServiceProvider services) | |||
} | |||
} | |||
|
|||
public override HttpMessageHandler PrimaryHandler { get; set; } = new HttpClientHandler(); | |||
private HttpMessageHandler? _primaryHandler; | |||
internal bool PrimaryHandlerIsSet { get; private set;} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed? Why can't _primaryHandler
just be checked directly to see whether it's null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because there are existing cases when primary handler is set explicitly to null. E.g. in gRPC. While I kinda consider this pattern erroneous, I still don't want to break the users...
/// Accessing this property will have a performance impact, as it will disable some optimizations. | ||
/// Consider using configuration APIs like ConfigurePrimaryHttpMessageHandler or | ||
/// ConfigureAdditionalHttpMessageHandlers instead of accessing this property directly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this regress existing code, or are the optimizations this disables only relevant for code using new features?
Debug.Assert(_primaryHandlerActions is not null); | ||
Debug.Assert(_additionalHandlersActions is not null); | ||
|
||
List<Action<HttpMessageHandlerBuilder>> handlerBuilderActions = new List<Action<HttpMessageHandlerBuilder>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
List<Action<HttpMessageHandlerBuilder>> handlerBuilderActions = new List<Action<HttpMessageHandlerBuilder>>(); | |
List<Action<HttpMessageHandlerBuilder>> handlerBuilderActions = new List<Action<HttpMessageHandlerBuilder>>(_primaryHandlerActions.Count + _additionalHandlersActions.Count); |
handlerBuilderActions.AddRange(_primaryHandlerActions); | ||
handlerBuilderActions.AddRange(_additionalHandlersActions); | ||
|
||
_primaryHandlerActions = null; | ||
_additionalHandlersActions = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have these lists been handed out to anyone externally? If not, rather than allocating a new list and throwing away both existing ones, you could do:
List<Action<HttpMessageHandlerBuilder>> handlerBuilderActions = _primaryHandlerActions;
handlerBuilderActions .AddRange(_additionalHandlersActions);
_primaryHandlerActions = null;
_additionalHandlersActions = null;
return handlerBuilderActions;
|
||
internal interface IAdditionalHandlersBuilder : IHandlerBuilder | ||
{ | ||
IList<DelegatingHandler> AdditionalHandlers { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For these internal types, can you use the concrete types instead of interfaces?
Is this opt in? What happens if I use a container that doesn't support keyed services? Will updating break me? |
Good catch @davidfowl, thanks. I naively thought that given that Turns out, it's not so easy. The saddest thing is that I see no way in checking whether it's a third-party container being used, let alone whether it supports our new keyed feature (I expect, for quite some time, none of them would). Because we are registering to our own Given the point in time, I guess the only way forward is to add an app-context switch for the registration, which will be off by default. 😢 |
Thinking more about this, and how we would have to have it off by default, I'm even considering not taking it into 8.0 at all... The integration request came a bit too late in the game, but we decided to go with it, because it is a new shiny feature that brings a lot of value, and also to avoid the following: if this would not be part of 8.0 - then someone could make their own keyed HttpClient registrations, and we would clash when we adopt it later in e.g. 9.0... However, now in light that we still have to have some kind of opt-in API and can't have it on by default, the clash doesn't worry me anymore since it will be controlled by the same API. Thoughts on punting this? @karelz @davidfowl @halter73 @JamesNK |
Agreed, I don't think it makes sense to rush adding new API post RC1. We can't catch the RC1 train now. |
I think it makes sense to do this early in 9 to better understand how it might break users. |
Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it. |
Draft Pull Request was automatically closed for 30 days of inactivity. Please let us know if you'd like to reopen it. |
@CarnaViire Lets do this early in .NET 9 so flesh out any issues that might be lurking 😄 |
@davidfowl yes, that's the plan 👍 I have my plate full at the moment, but I will proceed as soon as I'm able to |
Is there an issue outside of this PR? |
#89755 but I didn't update it to API proposal yet. I may not be able to proceed in November -- I have a couple of important time-sensitive project, so it's possible we might have to move it to early January (as we might not have a quorum on API reviews in December). Just setting expectations. It would still be an early phase of 9.0, so I think it's fine. |
HttpClient
(andHttpMessageHandler
chain) are registered as keyed transients with client name as the keyAddHttpMessageHandler
,ConfigurePrimaryHttpMessageHandler
andAddLogger
overloads are resolved as keyed by client name if possible, otherwise fall back to ordinary resolveTransient lifetime was chosen to make this
functionally equivalent to this
Especially if needed to use in singletons.
I've considered scoped lifetime, but it would be too confusing with typed clients being transient and named clients created by factory being essentially transient.
Scenarios made possible by (1):
Constructor injection with the attribute (can substitute Typed clients):
Resolving by name directly from service provider (needed when name is not a compile-time constant):
Scenarios made possible by (2):
Registering different implementations per name
Easy access to client name from implementation
Fixes #89755