Skip to content
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] ConfigureHttpClientDefaults for HttpClientFactory #87914

Closed
JamesNK opened this issue Jun 22, 2023 · 16 comments · Fixed by #87953
Closed

[API Proposal] ConfigureHttpClientDefaults for HttpClientFactory #87914

JamesNK opened this issue Jun 22, 2023 · 16 comments · Fixed by #87953
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-Extensions-HttpClientFactory blocking Marks issues that we want to fast track in order to unblock other important work
Milestone

Comments

@JamesNK
Copy link
Member

JamesNK commented Jun 22, 2023

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()));
@JamesNK JamesNK added api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Extensions-HttpClientFactory labels Jun 22, 2023
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Jun 22, 2023
@ghost
Copy link

ghost commented Jun 22, 2023

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and motivation

HttpClientFactory in Microsoft.Extensions.Http allows a developer to centrally build and configure 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 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

Author: JamesNK
Assignees: -
Labels:

api-suggestion, area-Extensions-HttpClientFactory

Milestone: -

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jun 23, 2023
@CarnaViire
Copy link
Member

CarnaViire commented Jun 23, 2023

We had a discussion with @stephentoub yesterday about the API shape and we came up with a potentially more easy-to-undersand API:

public static IServiceCollection ConfigureHttpClientDefaults(this IServiceCollection services, Action<IHttpClientBuilder> configure) {}

One of the main points was that AddHttpClientDefaults() itself is a "no-op" which doesn't bring any value if the builder is not subsequently configured. The API proposed requires a configuration method to be passed.
This API also mitigates the issue we've discussed on our call @JamesNK

I strongly believe we need 2 additional APIs to be able to conveniently modify default configuration per-name:

public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this IHttpClientBuilder builder, Action<HttpMessageHandler, IServiceProvider> configureHandler) {}
public static IHttpClientBuilder ConfigureAdditionalHttpMessageHandlers(this IHttpClientBuilder builder, Action<IList<DelegatingHandler>, IServiceProvider> configureAdditionalHandlers) {}

While this can be achieved through ConfigureHttpMessageHandlerBuilder, I would strongly advise against it.

I'd like to avoid encouraging users to use the API that forbids optimizations and that we are planning to eventually deprecate (as it prevents us from improvements and/or fixes like #35987 and #47091 that require separation of PrimaryHandler creation from additional handlers creation) -- and currently there's no other way.

Usages:

services.ConfigureHttpClientDefaults(b =>
    b.AddHttpMessageHandler<MyAuthHandler>()
        .ConfigurePrimaryHandler(() => new SocketsHttpHandler() { UseCookies = false }));

// will have MyAuthHandler and SocketsHttpHandler with UseCookies = false
services.AddHttpClient("foo");

// will have MyAuthHandler and SocketsHttpHandler with UseCookies = false and MaxConnectionsPerServer = 1
services.AddHttpClient("bar")
    .ConfigurePrimaryHandler((handler, _) => ((SocketsHttpHandler)handler).MaxConnectionsPerServer = 1);

// will have MyLoggingHandler as a top-most wrapper handler, MyAuthHandler, and SocketsHttpHandler
// with UseCookies = false
services.AddHttpClient("baz")
    .ConfigureAdditionalHttpMessageHandlers((handlerList, _) => handlerList.Insert(0, new MyLoggingHandler());

// will have SocketsHttpHandler with UseCookies = false and will not have MyAuthHandler
services.AddHttpClient("qux")
    .ConfigureAdditionalHttpMessageHandlers((handlerList, _) =>
        handlerList.Remove(handlerList.SingleOrDefault(h => h.GetType() == typeof(MyAuthHandler))));

@CarnaViire CarnaViire added this to the 8.0.0 milestone Jun 23, 2023
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Jun 23, 2023
@JamesNK
Copy link
Member Author

JamesNK commented Jun 25, 2023

We had a discussion with @stephentoub yesterday about the API shape and we came up with a potentially more easy-to-undersand API:

public static IServiceCollection ConfigureHttpClientDefaults(this IServiceCollection services, Action<IHttpClientBuilder> configure) {}

One of the main points was that AddHttpClientDefaults() itself is a "no-op" which doesn't bring any value if the builder is not subsequently configured. The API proposed requires a configuration method to be passed. This API also mitigates the issue we've discussed on our call @JamesNK

Good point that AddHttpClientDefaults() doesn't do anything by itself. However, it's worth mentioning that AddHttpClient("name") doesn't do anything either. It just returns a builder with the name, and that's it:

public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, string name)
{
ThrowHelper.ThrowIfNull(services);
ThrowHelper.ThrowIfNull(name);
AddHttpClient(services);
return new DefaultHttpClientBuilder(services, name);
}

I like how AddHttpClientDefaults() is so similar to AddHttpClient(...). But, I don't have a strong objection to ConfigureHttpClientDefaults. It solves the chaining issue.

I strongly believe we need 2 additional APIs to be able to conveniently modify default configuration per-name:

Do they need to be done right now as part of this issue? As far as I can tell they don't block the customer scenarios in this issue, and they aren't blocked from being done in the future.

Off topic, I noticed a problem in one of your sample usages:

// will have MyAuthHandler and SocketsHttpHandler with UseCookies = false and MaxConnectionsPerServer = 1
services.AddHttpClient("bar")
    .ConfigurePrimaryHandler((handler, _) => handler.MaxConnectionsPerServer = 1);

In the proposed API, this example is missing a cast from HttpMessageHandler to SocketsHttpHandler:

// will have MyAuthHandler and SocketsHttpHandler with UseCookies = false and MaxConnectionsPerServer = 1
services.AddHttpClient("bar")
    .ConfigurePrimaryHandler((handler, _) => ((SocketsHttpHandler)handler).MaxConnectionsPerServer = 1);

@CarnaViire
Copy link
Member

Do they need to be done right now as part of this issue? As far as I can tell they don't block the customer scenarios in this issue, and they aren't blocked from being done in the future.

I believe they need to be part of this release. While they don't block scenario of setting defaults, their absence subsequently encourages the use of ConfigureHttpMessageHandlerBuilder as the only workaround for per-name re-configuration. It will make it super painful to obsolete ConfigureHttpMessageHandlerBuilder in the future if that happens.

In the proposed API, this example is missing a cast from HttpMessageHandler to SocketsHttpHandler

Good catch, thanks! I'll update it.

@CarnaViire
Copy link
Member

CarnaViire commented Jun 27, 2023

Good point that AddHttpClientDefaults() doesn't do anything by itself. However, it's worth mentioning that AddHttpClient("name") doesn't do anything either. It just returns a builder with the name, and that's it

Good point, even though that's not strictly true, it will add all the HttpClientFactory infra the first time it's called... so you can use the factory to create (unconfigured/default) clients right away. And while AddHttpClientDefaults would also all the infra, it wouldn't bring any more value than an empty AddHttpClient call... Though, I liked the similarity as well. I suggest bringing both options to the API review and decide there.

2 additional APIs to be able to conveniently modify default configuration per-name

Do they need to be done right now as part of this issue?

Let me also add that they don't have to be optimized now, if you are worried about that. Non-optimized implementation is a straightforward one-liner.

@stephentoub
Copy link
Member

The concept of a parameterless method adding a new named client makes sense. The concept of a parameterless method adding defaults does not; it suggests without doing it there aren't defaults, which is both confusing and incorrect.

@JamesNK
Copy link
Member Author

JamesNK commented Jun 28, 2023

Updating to use ConfigureHttpClientDefaults: a13c14a (#87953)

Simple change. Feels good to use.

Add extra configure methods for additional handlers and primary handler: ec57e0a (#87953)

I implemented these by using the existing HttpMessageHandlerBuilderActions. That means the order the configuration runs in depends on when it is called.

What happens if someone does:

services.AddHttpClient("bar")
    .ConfigurePrimaryHandler((handler, _) => ((SocketsHttpHandler)handler).MaxConnectionsPerServer = 1);

services.AddHttpClient("bar")
    .ConfigurePrimaryHandler(() => new SocketsHttpHandler() { UseCookies = false }));

Exact thought needs to be put into when these methods run.

Another example:

services.AddHttpClient("bar")
    .AddHttpMessageHandler(() => Mock.Of<DelegatingHandler>())
    .ConfigureAdditionalHttpMessageHandlers((additionalHandlers, _) =>
    {
        additionalHandlers.Clear();
    });

Someone might expect the code above to result in no additional handlers for the client. Actually the client still has two additional handlers. After the delegate added by ConfigureAdditionalHttpMessageHandlers runs and clears the collection, configuration by the logging filter adds two logging handlers.

@JamesNK
Copy link
Member Author

JamesNK commented Jun 28, 2023

I think the intent is these configuration methods is they run after HttpMessageHandlerBuilderActions. Won't extra APIs be needed on HttpClientFactoryOptions?

public class HttpClientFactoryOptions
{
+   public IList<Action<HttpMessageHandler>> PrimaryMessageHandlerActions { get; }

+   public IList<Action<IList<HttpMessageHandler>> AdditionalHttpMessageHandlerActions { get; }
}

@CarnaViire
Copy link
Member

Thanks @JamesNK!

That means the order the configuration runs in depends on when it is called.

That was exactly the intent, and it aligns with how usually in DI registrations the last one "wins". So in the first example, the second ConfigurePrimaryHandler overwrites the previous handler, and that's expected.

Exact thought needs to be put into when these methods run.

The idea is that they are run in the same time they would previously run when people used ConfigureHttpMessageHandlerBuilder and allow for simple transition with the similar and expected functionality. Basically, implementation via HttpMessageHandlerBuilderActions is a baseline, and it further can be optimized by splitting into two collections PrimaryMessageHandlerActions and AdditionalHttpMessageHandlerActions, etc.

I think the intent is these configuration methods is they run after HttpMessageHandlerBuilderActions.

The final goal is that there would be no HttpMessageHandlerBuilderActions, so both collections PrimaryMessageHandlerActions and AdditionalHttpMessageHandlerActions could be run independently (but still depending on the order of registration within each group).

As soon as anything is added to HttpMessageHandlerBuilderActions, everything would need to fall back to single HttpMessageHandlerBuilderActions collection to retain order.

Won't extra APIs be needed on HttpClientFactoryOptions?

It's nice to have, but I believe it still can be done with them being internal.

Someone might expect the code above to result in no additional handlers for the client. Actually the client still has two additional handlers. After the delegate added by ConfigureAdditionalHttpMessageHandlers runs and clears the collection, configuration by the logging filter adds two logging handlers.

I would consider logging to be something a bit separate from other additional handlers, and with separate configuration to be discussed in #85840.

Also, the same would happen if you clear the collection via ConfigureHttpMessageHandlerBuilder so I would vote for them to have similar behavior.

@JamesNK
Copy link
Member Author

JamesNK commented Jun 28, 2023

I think the intent is these configuration methods is they run after HttpMessageHandlerBuilderActions.

The final goal is that there would be no HttpMessageHandlerBuilderActions, so both collections PrimaryMessageHandlerActions and AdditionalHttpMessageHandlerActions could be run independently (but still depending on the order of registration within each group).

As soon as anything is added to HttpMessageHandlerBuilderActions, everything would need to fall back to single HttpMessageHandlerBuilderActions collection to retain order.

I know you want people to stop using the build HttpMessageHandlerBuilderActions. I mentioned the builder actions as a shortcut for the extension methods that configure it internally: ConfigurePrimaryHttpMessageHandler, AddHttpMessageHandler.

Source:

/// <summary>
/// Adds a delegate that will be used to create an additional message handler for a named <see cref="HttpClient"/>.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <param name="configureHandler">A delegate that is used to create a <see cref="DelegatingHandler"/>.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
/// <remarks>
/// The <see paramref="configureHandler"/> delegate should return a new instance of the message handler each time it
/// is invoked.
/// </remarks>
public static IHttpClientBuilder AddHttpMessageHandler(this IHttpClientBuilder builder, Func<DelegatingHandler> configureHandler)
{
ThrowHelper.ThrowIfNull(builder);
ThrowHelper.ThrowIfNull(configureHandler);
builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>
{
options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(configureHandler()));
});
return builder;
}
/// <summary>
/// Adds a delegate that will be used to create an additional message handler for a named <see cref="HttpClient"/>.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <param name="configureHandler">A delegate that is used to create a <see cref="DelegatingHandler"/>.</param> /// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
/// <remarks>
/// <para>
/// The <see paramref="configureHandler"/> delegate should return a new instance of the message handler each time it
/// is invoked.
/// </para>
/// <para>
/// The <see cref="IServiceProvider"/> argument provided to <paramref name="configureHandler"/> will be
/// a reference to a scoped service provider that shares the lifetime of the handler being constructed.
/// </para>
/// </remarks>
public static IHttpClientBuilder AddHttpMessageHandler(this IHttpClientBuilder builder, Func<IServiceProvider, DelegatingHandler> configureHandler)
{
ThrowHelper.ThrowIfNull(builder);
ThrowHelper.ThrowIfNull(configureHandler);
builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>
{
options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(configureHandler(b.Services)));
});
return builder;
}
/// <summary>
/// Adds an additional message handler from the dependency injection container for a named <see cref="HttpClient"/>.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
/// <typeparam name="THandler">
/// The type of the <see cref="DelegatingHandler"/>. The handler type must be registered as a transient service.
/// </typeparam>
/// <remarks>
/// <para>
/// The <typeparamref name="THandler"/> will be resolved from a scoped service provider that shares
/// the lifetime of the handler being constructed.
/// </para>
/// </remarks>
public static IHttpClientBuilder AddHttpMessageHandler<THandler>(this IHttpClientBuilder builder)
where THandler : DelegatingHandler
{
ThrowHelper.ThrowIfNull(builder);
builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>
{
options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(b.Services.GetRequiredService<THandler>()));
});
return builder;
}
/// <summary>
/// Adds a delegate that will be used to configure the primary <see cref="HttpMessageHandler"/> for a
/// named <see cref="HttpClient"/>.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <param name="configureHandler">A delegate that is used to create an <see cref="HttpMessageHandler"/>.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
/// <remarks>
/// The <see paramref="configureHandler"/> delegate should return a new instance of the message handler each time it
/// is invoked.
/// </remarks>
public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this IHttpClientBuilder builder, Func<HttpMessageHandler> configureHandler)
{
ThrowHelper.ThrowIfNull(builder);
ThrowHelper.ThrowIfNull(configureHandler);
builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>
{
options.HttpMessageHandlerBuilderActions.Add(b => b.PrimaryHandler = configureHandler());
});
return builder;
}
/// <summary>
/// Adds a delegate that will be used to configure the primary <see cref="HttpMessageHandler"/> for a
/// named <see cref="HttpClient"/>.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <param name="configureHandler">A delegate that is used to create an <see cref="HttpMessageHandler"/>.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
/// <remarks>
/// <para>
/// The <see paramref="configureHandler"/> delegate should return a new instance of the message handler each time it
/// is invoked.
/// </para>
/// <para>
/// The <see cref="IServiceProvider"/> argument provided to <paramref name="configureHandler"/> will be
/// a reference to a scoped service provider that shares the lifetime of the handler being constructed.
/// </para>
/// </remarks>
public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this IHttpClientBuilder builder, Func<IServiceProvider, HttpMessageHandler> configureHandler)
{
ThrowHelper.ThrowIfNull(builder);
ThrowHelper.ThrowIfNull(configureHandler);
builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>
{
options.HttpMessageHandlerBuilderActions.Add(b => b.PrimaryHandler = configureHandler(b.Services));
});
return builder;
}
/// <summary>
/// Configures the primary <see cref="HttpMessageHandler"/> from the dependency injection container
/// for a named <see cref="HttpClient"/>.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
/// <typeparam name="THandler">
/// The type of the <see cref="DelegatingHandler"/>. The handler type must be registered as a transient service.
/// </typeparam>
/// <remarks>
/// <para>
/// The <typeparamref name="THandler"/> will be resolved from a scoped service provider that shares
/// the lifetime of the handler being constructed.
/// </para>
/// </remarks>
public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler<THandler>(this IHttpClientBuilder builder)
where THandler : HttpMessageHandler
{
ThrowHelper.ThrowIfNull(builder);
builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>
{
options.HttpMessageHandlerBuilderActions.Add(b => b.PrimaryHandler = b.Services.GetRequiredService<THandler>());
});
return builder;
}

If the new methods aren't intended to run after all the HttpMessageHandlerBuilderActions have been executed then I guess the current implementation in the PR is fine.

@CarnaViire
Copy link
Member

I mentioned the builder actions as a shortcut for the extension methods that configure it internally

I know. What I meant is something like the following that could be called instead of options.HttpMessageHandlerBuilderActions.Add(... in the methods above

internal void AddPrimaryHandlerAction(Action<IPrimaryHandlerBuilder> action, bool disregardPreviousActions)
{
    if (_primaryHandlerActions != null) // HttpMessageHandlerBuilderActions is empty, collections are separated
    {
        if (disregardPreviousActions)
        {
            _primaryHandlerActions.Clear();
        }
        _primaryHandlerActions.Add(action);
    }
    else
    {
        // can't optimize in case of a unified collection
        HttpMessageHandlerBuilderActions.Add(action);
    }
}

Or you meant access to that functionality from the user side as well?

@JamesNK
Copy link
Member Author

JamesNK commented Jun 28, 2023

I'm going to leave any more changes until after the API is approved.

It's important to be clear about the configuration's order with ConfigurePrimaryHttpMessageHandler and ConfigureAdditionalHttpMessageHandlers. What are the scenarios these methods will be used in? Have example code of things configured in different and unexpected orders and what you expect the result to be.

This is a subtle but important detail. Right now there isn't enough information about the order of configuration for these methods and their interaction with older methods.

@CarnaViire CarnaViire changed the title [API Proposal]: AddHttpClientDefaults [API Proposal] ConfigureHttpClientDefaults for HttpClientFactory Jun 30, 2023
@JamesNK JamesNK added api-ready-for-review API is ready for review, it is NOT ready for implementation api-suggestion Early API idea and discussion, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jul 3, 2023
@JamesNK
Copy link
Member Author

JamesNK commented Jul 3, 2023

@CarnaViire What's the status here? I'm going on leave in less than a week and there are outstanding questions. When will they be answered? When will this go to API review? Reminder: I can't go to API review because of timezones and you'll need to represent this.

@CarnaViire
Copy link
Member

CarnaViire commented Jul 3, 2023

What's the status here?

I'm preparing the description for the API review, together with #84075 and #77312

When will this go to API review?

We don't have a date yet, but I've let Immo know, as I'd like to go over all HttpClientFactory APIs in one session

you'll need to represent this

That's what I intended to do, yes

there are outstanding questions. When will they be answered?

What exact questions are not answered? I might be missing something, but I believe I've commented on the expected behavior previously here #87914 (comment) (expected = exactly how it would behave if it was done by ConfigureHttpMessageHandlerBuilder instead), and some examples were here #87914 (comment) and I will add some more for the API review.

I'm going on leave in less than a week

I can take over the PR if needed, I believe if there would be any changes, they would be minor, like method names and such

@JamesNK
Copy link
Member Author

JamesNK commented Jul 3, 2023

Thanks, I wasn’t sure if the order question was answered.

@CarnaViire CarnaViire added blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Jul 4, 2023
@terrajobst
Copy link
Member

terrajobst commented Jul 11, 2023

Video

  • Looks good as proposed
namespace Microsoft.Extensions.DependencyInjection;

public static partial class HttpClientFactoryServiceCollectionExtensions
{
    public static IServiceCollection ConfigureHttpClientDefaults(
        this IServiceCollection services,
        Action<IHttpClientBuilder> configure);
}

public static partial class HttpClientBuilderExtensions
{
    // Existing:
    //
    // public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(
    //    this IHttpClientBuilder builder,
    //    Func<HttpMessageHandler> configureHandler);
    //
    // public static IHttpClientBuilder AddHttpMessageHandler(
    //    this IHttpClientBuilder builder,
    //    Func<DelegatingHandler> configureHandler);

    // 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 API which we should obsolete

    [Obsolete(...)] // Should point to ConfigureAdditionalHttpMessageHandlers 
    public static IHttpClientBuilder ConfigureHttpMessageHandlerBuilder(
        this IHttpClientBuilder builder,
        Action<HttpMessageHandlerBuilder> configureBuilder);
}

@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jul 11, 2023
CarnaViire added a commit that referenced this issue Jul 14, 2023
The AddHttpClientDefaults method supports adding configuration to all created HttpClients.

The method:

- Creates a builder with a null name. Microsoft.Extensions.Configuration automatically applies configuration with a null name to all named configuration.
- Ensures that default configuration is added before named configuration in the IServiceCollection. This is to make it so the order of AddHttpClientDefaults and AddHttpClient doesn't matter. Default config is always applied first, then named config is applied after. This is done by wrapping the IServiceCollection in an implementation that modifies the order that IConfigureOptions<HttpClientFactoryOptions> values are added.

Fixes #87914

---------

Co-authored-by: Natalia Kondratyeva <knatalia@microsoft.com>
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jul 14, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Aug 13, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-Extensions-HttpClientFactory blocking Marks issues that we want to fast track in order to unblock other important work
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants