Description
Original issue by @JamesNK
Background and motivation
HttpClientFactory
in Microsoft.Extensions.Http allows a developer to centrally configure and instatiate HTTP clients with DI:
services
.AddHttpClient("consoto", c => c.BaseAddress = new Uri("https://consoto.com/"))
.AddHttpMessageHandler<MyAuthHandler>();
services
.AddHttpClient("github", c => c.BaseAddress = new Uri("https://github.com/"))
.AddHttpMessageHandler<MyAuthHandler>();
Two named clients are defined in the example above. Unfortunately, there isn't a way to apply the default settings for all clients. Both need AddHttpMessageHandler<MyAuthHandler>()
.
This issue proposes an AddHttpClientDefaults
method that can be used to specify configuration that is applied to all clients.
API Proposal
namespace Microsoft.Extensions.DependencyInjection;
public static class HttpClientFactoryServiceCollectionExtensions
{
+ public static IHttpClientBuilder AddHttpClientDefaults(this IServiceCollection services);
}
API Usage
AddHttpClientDefaults
returns an IHttpClientBuilder
. This is the same type returned by services.AddHttpClient(...)
. That means all the extension methods for IHttpClientBuilder
automatically work with AddHttpClientDefaults
.
services.AddHttpClientDefaults()
.AddHttpMessageHandler<MyAuthHandler>();
// Clients automatically have the handler specified as a default.
services.AddHttpClient("consoto", c => c.BaseAddress = new Uri("https://consoto.com/"));
services.AddHttpClient("github", c => c.BaseAddress = new Uri("https://github.com/"));
The default configuration is run on a client before client-specific configuration:
services.AddHttpClientDefaults()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://consoto.com/"));
// Client a base address of https://consoto.com
services.AddHttpClient("consoto");
// Client has a base address of https://github.com/ (overrides default)
services.AddHttpClient("github", c => c.BaseAddress = new Uri("https://github.com/"));
Default configuration can be added to multiple times, and its order compared to AddHttpClient
doesn't matter:
services.AddHttpClientDefaults()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://consoto.com/"));
// Client a base address of https://consoto.com and has MyAuthHandler
services.AddHttpClient("consoto");
services.AddHttpClientDefaults()
.AddHttpMessageHandler<MyAuthHandler>();
Alternative Designs
We want AddHttpClientDefaults
that returns an IHttpClientBuilder
so existing extension methods automatically work with it.
We want to avoid the situation that we are seeing where people are adding two extension methods: one to add configuration to a builder, and another to apply configuration to all builders.
Example of what we don't want:
// Add handler to one client
services.AddHttpClient("myclient").AddMyCustomerLogging();
// Add handler to all clients
services.AddMyCustomerLoggingToAllClients();
Risks
No response
Background and motivation
HttpClientFactory
in Microsoft.Extensions.Http allows a developer to configure and create HttpClient instances with DI.
Consider an example where two named clients are defined, and they all need MyAuthHandler in their message handlers chain.
services
.AddHttpClient("consoto", c => c.BaseAddress = new Uri("https://consoto.com/"))
.AddHttpMessageHandler<MyAuthHandler>();
services
.AddHttpClient("github", c => c.BaseAddress = new Uri("https://github.com/"))
.AddHttpMessageHandler<MyAuthHandler>();
Unfortunately, there isn't a way to apply the default settings for all clients. All need AddHttpMessageHandler<MyAuthHandler>()
.
This issue proposes an ConfigureHttpClientDefaults
method that can be used to specify configuration that is applied to all clients.
API Proposal
// existing
public static class HttpClientFactoryServiceCollectionExtensions
{
// new
public static IServiceCollection ConfigureHttpClientDefaults(
this IServiceCollection services, Action<IHttpClientBuilder> configure) {}
}
// existing
public static class HttpClientBuilderExtensions
{
// new
public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(
this IHttpClientBuilder builder,
Action<HttpMessageHandler, IServiceProvider> configureHandler) {}
public static IHttpClientBuilder ConfigureAdditionalHttpMessageHandlers(
this IHttpClientBuilder builder,
Action<IList<DelegatingHandler>, IServiceProvider> configureAdditionalHandlers) {}
// existing (+2 overloads)
// public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this IHttpClientBuilder builder, Func<HttpMessageHandler> configureHandler) {}
// existing (+2 overloads)
// public static IHttpClientBuilder AddHttpMessageHandler(this IHttpClientBuilder builder, Func<DelegatingHandler> configureHandler) {}
// existing -- to be deprecated in the future
// public static IHttpClientBuilder ConfigureHttpMessageHandlerBuilder(this IHttpClientBuilder builder, Action<HttpMessageHandlerBuilder> configureBuilder) {}
}
API Usage
1. Basic usage and extension methods
ConfigureHttpClientDefaults
accepts an action over IHttpClientBuilder
. This is the same type returned by services.AddHttpClient(...)
. That means all the extension methods for IHttpClientBuilder
automatically work with AddHttpClientDefaults
.
services.ConfigureHttpClientDefaults(b =>
b.AddHttpMessageHandler<MyAuthHandler>());
// Clients automatically have the handler specified as a default.
services.AddHttpClient("consoto", c => c.BaseAddress = new Uri("https://consoto.com/"));
services.AddHttpClient("github", c => c.BaseAddress = new Uri("https://github.com/"));
2. Order of applying
The default configuration is run on a client before client-specific configuration:
services.ConfigureHttpClientDefaults(b =>
b.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://consoto.com/")));
// Client a base address of https://consoto.com
services.AddHttpClient("consoto");
// Client has a base address of https://github.com/ (overrides default)
services.AddHttpClient("github", c => c.BaseAddress = new Uri("https://github.com/"));
Default configuration can be added to multiple times, between each other it will be applied one-by-one in order of registration; and its place in the registration compared to AddHttpClient
doesn't matter:
services.ConfigureHttpClientDefaults(b =>
b.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://consoto.com/")));
// Client a base address of https://consoto.com and has MyAuthHandler
services.AddHttpClient("consoto");
services.AddHttpClientDefaults()
.AddHttpMessageHandler<MyAuthHandler>();
3. Subsequent per-name reconfiguration
Setting additional properties in a primary handler:
services.ConfigureHttpClientDefaults(b =>
b.ConfigurePrimaryHandler(() => new SocketsHttpHandler() { UseCookies = false }));
// will have SocketsHttpHandler with UseCookies = false and MaxConnectionsPerServer = 1
services.AddHttpClient("bar")
.ConfigurePrimaryHandler((handler, _) => ((SocketsHttpHandler)handler).MaxConnectionsPerServer = 1);
Inserting in a specific position or removing from an additional handlers list:
services.ConfigureHttpClientDefaults(b =>
b.AddHttpMessageHandler<MyAuthHandler>());
// will have MyLoggingHandler as a top-most wrapper handler, and MyAuthHandler
services.AddHttpClient("baz")
.ConfigureAdditionalHttpMessageHandlers((handlerList, _) => handlerList.Insert(0, new MyLoggingHandler());
// will NOT have MyAuthHandler
services.AddHttpClient("qux")
.ConfigureAdditionalHttpMessageHandlers((handlerList, _) =>
handlerList.Remove(handlerList.SingleOrDefault(h => h.GetType() == typeof(MyAuthHandler))));
4. Subsequent per-name reconfiguration -- order of applying
Between each other, the order of ConfigurePrimaryHandler
and ConfigureAdditionalHttpMessageHandlers
and other per-name configuration methods is the same as if it was implemented via ConfigureHttpMessageHandlerBuilder
-- in order of registration, and if a subsequent write overwrites e.g. a primary handler instance, the last one "wins".
// Primary handler will already be a SocketsHttpHandler instance because it was set in ConfigureHttpClientDefaults
// below, which runs first, so it is safe to cast
services.AddHttpClient("bar")
.ConfigurePrimaryHandler((handler, _) => ((SocketsHttpHandler)handler).MaxConnectionsPerServer = 1);
// But the second call overwrites primary handler, so the final result will NOT have MaxConnectionsPerServer set
services.AddHttpClient("bar")
.ConfigurePrimaryHandler(() => new SocketsHttpHandler() { UseCookies = false }));
services.ConfigureHttpClientDefaults(b =>
b.ConfigurePrimaryHandler(() => new SocketsHttpHandler()));