Skip to content

[API Proposal] ConfigureHttpClientDefaults for HttpClientFactory #87914

Closed
@JamesNK

Description

@JamesNK
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()));

Metadata

Metadata

Labels

api-approvedAPI was approved in API review, it can be implementedarea-Extensions-HttpClientFactoryblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions