-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
[API Proposal]: Consider adding methods to the HTTP Client factory that receive Type parameters instead of generic type parameters #104525
Comments
Tagging subscribers to this area: @dotnet/ncl |
Would you mind sharing an example of what you are doing? I fail to see why you are not using named (but untyped) HTTP client registrations and using |
Instead of explicitly registering the HTTP clients in code, I use configuration for that. At startup, I add all the HTTP clients in the configuration. And the configuration might have type HTTP clients. |
@paulomorgado I think you misunderstood what I asked. I 100% understand that you are dynamically registering the clients based on configuration, but my point specifically was that you don't need to use the typed registration methods to achieve that, you can just used named registrations instead and then inject If you know how WCF config-based bindings worked in the past, this would be very similar to that. |
I know I can do that, but my components have the typed clients injected and I never inject the Just because I can inject |
Fair enough. I was just checking whether the named registrations could be a way for your use case: looks like it isn't. Would still be nice if you could share a sample of your problem to make a stronger case for the proposed API. In the meantime, you could just register the types yourself using services.AddHttpClient(myHttpClientName, //...configure here...
services.AddTransient(myInterface, provider => ActivatorUtilities.CreateInstance(
provider,
myType,
provider.GetRequiredService<IHttpClientFactory>().CreateClient(myHttpClientName)) Which you could, of course, abstract away into your own generic helper method. The problem I see with the original |
I have a working solution. I just don't think this is my requirement only and the factory usability would benefit from this. |
Again, I would suggest sharing your use case and your current workaround so more people can see the benefit of the new API and can rely on your workaround in the meantime.
I've never seen anyone else request this myself. All dynamic registration discussions revolved around named httpclient registrations or even using the new keyed registration support. |
I am dynamically registering typed HTTP clients. The particulars of why I need to do it are not important nor I will discuss them publicly. |
Thanks for the proposal @paulomorgado Triage: There are already 20 (!) overloads of Please let me know if I missed something, or there's some additional reasons why we should reconsider. Thanks! |
This is what I have working now (in case anyone needs it): public static class MyHttpClientFactoryDependencyInjectionExtensions
{
private static readonly MethodInfo _addHttpClientByClientTypeMethodInfo =
typeof(HttpClientFactoryServiceCollectionExtensions)
.GetMethod(
nameof(HttpClientFactoryServiceCollectionExtensions.AddHttpClient),
1,
new Type[] { typeof(IServiceCollection) })!;
private static readonly MethodInfo _addHttpClientByClientTypeWithNameMethodInfo =
typeof(HttpClientFactoryServiceCollectionExtensions)
.GetMethod(
nameof(HttpClientFactoryServiceCollectionExtensions.AddHttpClient),
1,
new Type[] { typeof(IServiceCollection), typeof(string) })!;
private static readonly MethodInfo _addHttpClientByClientAndClientImplementationTypeMethodInfo =
typeof(HttpClientFactoryServiceCollectionExtensions)
.GetMethod(
nameof(HttpClientFactoryServiceCollectionExtensions.AddHttpClient),
2,
new Type[] { typeof(IServiceCollection) })!;
private static readonly MethodInfo _addHttpClientByClientAndClientImplementationTypeWithNameMethodInfo =
typeof(HttpClientFactoryServiceCollectionExtensions)
.GetMethod(
nameof(HttpClientFactoryServiceCollectionExtensions.AddHttpClient),
2,
new Type[] { typeof(IServiceCollection), typeof(string) })!;
public static IServiceCollection AddHttpClients(this IServiceCollection services, IConfiguration configuration)
{
// ...
foreach (var section in configuration.GetChildren())
{
var name = section.Key;
var clientTypeName = section.GetSection("ClientType")?.Value ?? name;
var clientImplementationTypeName = section.GetSection("ClientImplementationType")?.Value;
var clientType = Type.GetType(clientTypeName);
var clientImplementationType = string.IsNullOrEmpty(clientImplementationTypeName)
? null
: Type.GetType(clientImplementationTypeName) ?? throw new InvalidOperationException($"The implementation type '{clientImplementationTypeName}' was not found for client '{name}'.");
if (clientImplementationType is not null && clientType is null)
{
throw new InvalidOperationException($"The implementation type '{clientImplementationType.AssemblyQualifiedName}' was configured, but the client type '{clientTypeName}' was not found for client '{name}'.");
}
var http = clientType is null
? services.AddHttpClient(name)
: clientImplementationType is null
? services.AddHttpClient(clientType, name)
: services.AddHttpClient(clientType, clientImplementationType, name);
http
.ConfigureHttpClient(httpClient =>
{
// ...
});
ConfigureClientCredentialsTokenHandler(http, section);
ConfigureStandardResilience(http, section);
}
return services;
}
public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, Type clientType)
=> (IHttpClientBuilder) _addHttpClientByClientTypeMethodInfo
.MakeGenericMethod(clientType)
.Invoke(null, new object[] { services })!;
public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, Type clientType, string name)
=> (IHttpClientBuilder) _addHttpClientByClientTypeWithNameMethodInfo
.MakeGenericMethod(clientType)
.Invoke(null, new object[] { services, name })!;
public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, Type clientType, Type clientImplementationType)
=> (IHttpClientBuilder) _addHttpClientByClientAndClientImplementationTypeMethodInfo
.MakeGenericMethod(clientType, clientImplementationType)
.Invoke(null, new object[] { services })!;
public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, Type clientType, Type clientImplementationType, string name)
=> (IHttpClientBuilder) _addHttpClientByClientAndClientImplementationTypeWithNameMethodInfo
.MakeGenericMethod(clientType, clientImplementationType)
.Invoke(null, new object[] { services, name })!;
} |
Thanks for sharing @paulomorgado! While that would 100% align with what the factory is doing with the Typed clients (given the actual methods are used), for the sake of simplicity let me also save here how it would look like with the Activator approach: public static class MyHttpClientFactoryDependencyInjectionExtensions
{
public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, string name, Type? clientType, Type? clientImplementationType)
{
if (clientImplementationType is not null && clientType is null)
{
throw new InvalidOperationException($"The implementation type '{clientImplementationType.AssemblyQualifiedName}' was configured, but the client type '{clientTypeName}' was not found for client '{name}'.");
}
if (clientType is not null)
{
clientImplementationType ??= clientType;
services.AddTransient(clientType, serviceProvider =>
ActivatorUtilities.CreateInstance(
serviceProvider,
clientImplementationType,
serviceProvider.GetRequiredService<IHttpClientFactory>().CreateClient(name)));
}
return services.AddHttpClient(name);
} This could be expanded further to e.g. cache the object activator method like (technically even The only problem is that if e.g. #89755 gets implemented, then the actual Typed client registration might change a bit (e.g. getting a client as a keyed service instead of a direct static HttpClient CreateHttpClient(IServiceProvider serviceProvider, string name)
{
HttpClient? fromKeyed = null;
if (serviceProvider is IKeyedServiceProvider keyedServiceProvider)
{
fromKeyed = keyedServiceProvider.GetKeyedService<HttpClient>(name);
}
return fromKeyed ?? serviceProvider.GetRequiredService<IHttpClientFactory>().CreateClient(name);
} (-- or maybe even switch from typed clients to services with a keyed dependency?) |
Whatever reason justifies having typed clients in By the way, I had created a HTTP client factory that loaded definitions from a catalog before .NET Core had My catalogue already as a feature (omitted in the code) to register a keyed Also, there's a special keyed As for the |
If this feature is already used in production -- would you mind sharing more re: your experience with keyed clients? (here or in the related issue #89755) I don't know the circumstances in which your catalogue is used, but I'm still curious about the perspective. I'm asking because there are some tricky issues, like HttpClient being IDisposable, which leads to it being captured by DI. I wonder whether you've hit them in practice. Also -- if HttpClientFactory ends up registering keyed clients by default, will that break your code?
That is a very interesting idea, actually 👀
Yeah, I know it's a pain. Sorry about that 😞 |
So far, I haven't hit any issues regarding If clients are registered as keyed, I'll have to remove some code. 😄 |
I've been thinking about it a bit more, and I believe if we add an overload to services.AddHttpClient(name)
.AddTypedClient(clientType, clientImplementationType); (It's not as heavy as I think I can resurrect the issue (and this can also help to gauge interest) I'd still have to triage it to Future as a nice-to-have at this point (just setting expectations) |
If you are interested in the feature, please upvote the top post, it will help us prioritize. Thanks! |
That would work for me just fine. I wouldn't even mind not having services.AddHttpClient(name)
.AddTypedClient(clientType); but I'm sure someone will complain about that. 😄 |
From my perspective, the issue is not really the specific methods being suggested, those are completely fine. I have a problem with perpetuating this entire set of special-cased IMHO, the more we keep piling on it, the worse it will become overall. I've made a comment here with a bit more detail on that thought: To me, none of these APIs should even exist. They should be replaced and deprecated. |
Updated by @CarnaViire
API:
Usage:
Original issue by @paulomorgado
Background and motivation
Sometimes I have the need to register HTTP clients from configuration files and, for that, I need to construct a generic method for each type.
API Proposal
API Usage
Alternative Designs
No response
Risks
There might be a risk for overloads with factory methods, as correctness might be hard to guarantee.
The text was updated successfully, but these errors were encountered: