diff --git a/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs b/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs index ec14a145c0394a..b5265bec6acc50 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs +++ b/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs @@ -164,9 +164,24 @@ internal ActiveHandlerTrackingEntry CreateHandlerEntry(string name) void Configure(HttpMessageHandlerBuilder b) { - for (int i = 0; i < options.HttpMessageHandlerBuilderActions.Count; i++) + if (options.MergedToHandlerBuilderActions) { - options.HttpMessageHandlerBuilderActions[i](b); + for (int i = 0; i < options.HttpMessageHandlerBuilderActions.Count; i++) + { + options.HttpMessageHandlerBuilderActions[i](b); + } + } + else + { + 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); + } } // Logging is added separately in the end. But for now it should be still possible to override it via filters... diff --git a/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpMessageHandlerBuilder.cs b/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpMessageHandlerBuilder.cs index 21b39a15b121e2..c55d5a5e270079 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpMessageHandlerBuilder.cs +++ b/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpMessageHandlerBuilder.cs @@ -28,7 +28,26 @@ public override string? Name } } - public override HttpMessageHandler PrimaryHandler { get; set; } = new HttpClientHandler(); + private HttpMessageHandler? _primaryHandler; + internal bool PrimaryHandlerIsSet { get; private set;} + public override HttpMessageHandler PrimaryHandler + { + get + { + if (_primaryHandler is null && !PrimaryHandlerIsSet) + { + _primaryHandler = new HttpClientHandler(); + PrimaryHandlerIsSet = true; + } + return _primaryHandler!; + } + + set + { + _primaryHandler = value; + PrimaryHandlerIsSet = true; + } + } public override IList AdditionalHandlers { get; } = new List(); diff --git a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.Logging.cs b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.Logging.cs index f87aff245cd6f1..0ccf7d979fe460 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.Logging.cs +++ b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.Logging.cs @@ -53,8 +53,8 @@ public static IHttpClientBuilder AddLogger(this IHttpClientBuilder builder, Func options.LoggingBuilderActions.Add(b => { IHttpClientLogger httpClientLogger = httpClientLoggerFactory(b.Services); - HttpClientLoggerHandler handler = new HttpClientLoggerHandler(httpClientLogger); + HttpClientLoggerHandler handler = new HttpClientLoggerHandler(httpClientLogger); if (wrapHandlersPipeline) { b.AdditionalHandlers.Insert(0, handler); @@ -110,7 +110,30 @@ public static IHttpClientBuilder AddLogger(this IHttpClientBuilder buil { ThrowHelper.ThrowIfNull(builder); - return AddLogger(builder, services => services.GetRequiredService(), wrapHandlersPipeline); + builder.Services.Configure(builder.Name, options => + { + options.LoggingBuilderActions.Add(b => + { + IHttpClientLogger? httpClientLogger = null; + if (b.Services is IKeyedServiceProvider keyedProvider) + { + httpClientLogger = keyedProvider.GetKeyedService(b.Name); + } + httpClientLogger ??= b.Services.GetRequiredService(); + + HttpClientLoggerHandler handler = new HttpClientLoggerHandler(httpClientLogger); + if (wrapHandlersPipeline) + { + b.AdditionalHandlers.Insert(0, handler); + } + else + { + b.AdditionalHandlers.Add(handler); + } + }); + }); + + return builder; } /// diff --git a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.cs b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.cs index 4ccf2432c5ee16..01e5be73095d45 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.cs @@ -78,7 +78,7 @@ public static IHttpClientBuilder AddHttpMessageHandler(this IHttpClientBuilder b builder.Services.Configure(builder.Name, options => { - options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(configureHandler())); + options.AddAdditionalHandlersAction(b => b.AdditionalHandlers.Add(configureHandler())); }); return builder; @@ -106,7 +106,7 @@ public static IHttpClientBuilder AddHttpMessageHandler(this IHttpClientBuilder b builder.Services.Configure(builder.Name, options => { - options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(configureHandler(b.Services))); + options.AddAdditionalHandlersAction(b => b.AdditionalHandlers.Add(configureHandler(b.Services))); }); return builder; @@ -133,7 +133,17 @@ public static IHttpClientBuilder AddHttpMessageHandler(this IHttpClien builder.Services.Configure(builder.Name, options => { - options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(b.Services.GetRequiredService())); + options.AddAdditionalHandlersAction(b => + { + THandler? handler = null; + if (b.Services is IKeyedServiceProvider keyedProvider) + { + handler = keyedProvider.GetKeyedService(b.Name); + } + handler ??= b.Services.GetRequiredService(); + + b.AdditionalHandlers.Add(handler); + }); }); return builder; @@ -157,7 +167,7 @@ public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this IHttpCl builder.Services.Configure(builder.Name, options => { - options.HttpMessageHandlerBuilderActions.Add(b => b.PrimaryHandler = configureHandler()); + options.AddPrimaryHandlerAction(b => b.PrimaryHandler = configureHandler(), updatesExistingHandler: false); }); return builder; @@ -187,7 +197,7 @@ public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this IHttpCl builder.Services.Configure(builder.Name, options => { - options.HttpMessageHandlerBuilderActions.Add(b => b.PrimaryHandler = configureHandler(b.Services)); + options.AddPrimaryHandlerAction(b => b.PrimaryHandler = configureHandler(b.Services), updatesExistingHandler: false); }); return builder; @@ -215,7 +225,18 @@ public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(th builder.Services.Configure(builder.Name, options => { - options.HttpMessageHandlerBuilderActions.Add(b => b.PrimaryHandler = b.Services.GetRequiredService()); + options.AddPrimaryHandlerAction(b => + { + THandler? handler = null; + if (b.Services is IKeyedServiceProvider keyedProvider) + { + handler = keyedProvider.GetKeyedService(b.Name); + } + handler ??= b.Services.GetRequiredService(); + + b.PrimaryHandler = handler; + }, + updatesExistingHandler: false); }); return builder; @@ -241,7 +262,7 @@ public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this IHttpCl builder.Services.Configure(builder.Name, options => { - options.HttpMessageHandlerBuilderActions.Add(b => configureHandler(b.PrimaryHandler, b.Services)); + options.AddPrimaryHandlerAction(b => configureHandler(b.PrimaryHandler, b.Services), updatesExistingHandler: true); }); return builder; @@ -288,15 +309,22 @@ public static IHttpClientBuilder UseSocketsHttpHandler(this IHttpClientBuilder b builder.Services.Configure(builder.Name, options => { - options.HttpMessageHandlerBuilderActions.Add(b => + options.AddPrimaryHandlerAction(b => { - if (b.PrimaryHandler is not SocketsHttpHandler handler) + SocketsHttpHandler? handler = null; + + // checking for PrimaryHandlerIsSet first avoids allocating a default handler instance on accessing PrimaryHandler property + bool canAccessPrimaryHandler = b is not DefaultHttpMessageHandlerBuilder dfb || dfb.PrimaryHandlerIsSet; + if (canAccessPrimaryHandler) { - handler = new SocketsHttpHandler(); + handler = b.PrimaryHandler as SocketsHttpHandler; } + handler ??= new SocketsHttpHandler(); + configureHandler?.Invoke(handler, b.Services); b.PrimaryHandler = handler; - }); + }, + updatesExistingHandler: true); }); return builder; @@ -378,6 +406,13 @@ public static IHttpClientBuilder UseSocketsHttpHandler(this IHttpClientBuilder b builder.Services.AddTransient(s => AddTransientHelper(s, builder)); + builder.Services.AddKeyedTransient(builder.Name, (s, key) => + { + HttpClient httpClient = s.GetRequiredKeyedService(key); + ITypedHttpClientFactory typedClientFactory = s.GetRequiredService>(); + return typedClientFactory.CreateClient(httpClient); + }); + return builder; } @@ -442,6 +477,13 @@ private static TClient AddTransientHelper(IServiceProvider s, IHttpClie builder.Services.AddTransient(s => AddTransientHelper(s, builder)); + builder.Services.AddKeyedTransient(builder.Name, (s, key) => + { + HttpClient httpClient = s.GetRequiredKeyedService(key); + ITypedHttpClientFactory typedClientFactory = s.GetRequiredService>(); + return typedClientFactory.CreateClient(httpClient); + }); + return builder; } @@ -499,6 +541,12 @@ internal static IHttpClientBuilder AddTypedClientCore(this IHttpClientB return factory(httpClient); }); + builder.Services.AddKeyedTransient(builder.Name, (s, key) => + { + HttpClient httpClient = s.GetRequiredKeyedService(key); + return factory(httpClient); + }); + return builder; } @@ -548,6 +596,12 @@ internal static IHttpClientBuilder AddTypedClientCore(this IHttpClientB return factory(httpClient, s); }); + builder.Services.AddKeyedTransient(builder.Name, (s, key) => + { + HttpClient httpClient = s.GetRequiredKeyedService(key); + return factory(httpClient, s); + }); + return builder; } @@ -644,7 +698,7 @@ public static IHttpClientBuilder ConfigureAdditionalHttpMessageHandlers(this IHt builder.Services.Configure(builder.Name, options => { - options.HttpMessageHandlerBuilderActions.Add(b => configureAdditionalHandlers(b.AdditionalHandlers, b.Services)); + options.AddAdditionalHandlersAction(b => configureAdditionalHandlers(b.AdditionalHandlers, b.Services)); }); return builder; diff --git a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientFactoryServiceCollectionExtensions.cs b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientFactoryServiceCollectionExtensions.cs index e5dffcfe253401..b1444fe57545fd 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientFactoryServiceCollectionExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientFactoryServiceCollectionExtensions.cs @@ -105,6 +105,7 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, ThrowHelper.ThrowIfNull(name); AddHttpClient(services); + services.AddKeyedHttpClient(name); return new DefaultHttpClientBuilder(services, name); } @@ -133,6 +134,7 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, ThrowHelper.ThrowIfNull(configureClient); AddHttpClient(services); + services.AddKeyedHttpClient(name); var builder = new DefaultHttpClientBuilder(services, name); builder.ConfigureHttpClient(configureClient); @@ -163,6 +165,7 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, ThrowHelper.ThrowIfNull(configureClient); AddHttpClient(services); + services.AddKeyedHttpClient(name); var builder = new DefaultHttpClientBuilder(services, name); builder.ConfigureHttpClient(configureClient); @@ -200,6 +203,8 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, AddHttpClient(services); string name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false); + services.AddKeyedHttpClient(name); + var builder = new DefaultHttpClientBuilder(services, name); builder.AddTypedClientCore(validateSingleType: true); return builder; @@ -241,6 +246,8 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, AddHttpClient(services); string name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false); + services.AddKeyedHttpClient(name); + var builder = new DefaultHttpClientBuilder(services, name); builder.AddTypedClientCore(validateSingleType: true); return builder; @@ -279,8 +286,10 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, ThrowHelper.ThrowIfNull(name); AddHttpClient(services); + services.AddKeyedHttpClient(name); var builder = new DefaultHttpClientBuilder(services, name); + builder.AddTypedClientCore(validateSingleType: false); // Name was explicitly provided. return builder; } @@ -323,6 +332,7 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, ThrowHelper.ThrowIfNull(name); AddHttpClient(services); + services.AddKeyedHttpClient(name); var builder = new DefaultHttpClientBuilder(services, name); builder.AddTypedClientCore(validateSingleType: false); // name was explicitly provided @@ -362,6 +372,8 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, AddHttpClient(services); string name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false); + services.AddKeyedHttpClient(name); + var builder = new DefaultHttpClientBuilder(services, name); builder.ConfigureHttpClient(configureClient); builder.AddTypedClientCore(validateSingleType: true); @@ -401,6 +413,8 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, AddHttpClient(services); string name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false); + services.AddKeyedHttpClient(name); + var builder = new DefaultHttpClientBuilder(services, name); builder.ConfigureHttpClient(configureClient); builder.AddTypedClientCore(validateSingleType: true); @@ -445,6 +459,8 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, AddHttpClient(services); string name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false); + services.AddKeyedHttpClient(name); + var builder = new DefaultHttpClientBuilder(services, name); builder.ConfigureHttpClient(configureClient); builder.AddTypedClientCore(validateSingleType: true); @@ -489,6 +505,8 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, AddHttpClient(services); string name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false); + services.AddKeyedHttpClient(name); + var builder = new DefaultHttpClientBuilder(services, name); builder.ConfigureHttpClient(configureClient); builder.AddTypedClientCore(validateSingleType: true); @@ -530,6 +548,7 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, ThrowHelper.ThrowIfNull(configureClient); AddHttpClient(services); + services.AddKeyedHttpClient(name); var builder = new DefaultHttpClientBuilder(services, name); builder.ConfigureHttpClient(configureClient); @@ -572,6 +591,7 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, ThrowHelper.ThrowIfNull(configureClient); AddHttpClient(services); + services.AddKeyedHttpClient(name); var builder = new DefaultHttpClientBuilder(services, name); builder.ConfigureHttpClient(configureClient); @@ -619,6 +639,7 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, ThrowHelper.ThrowIfNull(configureClient); AddHttpClient(services); + services.AddKeyedHttpClient(name); var builder = new DefaultHttpClientBuilder(services, name); builder.ConfigureHttpClient(configureClient); @@ -666,6 +687,7 @@ public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, ThrowHelper.ThrowIfNull(configureClient); AddHttpClient(services); + services.AddKeyedHttpClient(name); var builder = new DefaultHttpClientBuilder(services, name); builder.ConfigureHttpClient(configureClient); @@ -706,6 +728,8 @@ public static IHttpClientBuilder AddHttpClient(this IS ThrowHelper.ThrowIfNull(factory); string name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false); + services.AddKeyedHttpClient(name); + return AddHttpClient(services, name, factory); } @@ -746,6 +770,7 @@ public static IHttpClientBuilder AddHttpClient(this IS ThrowHelper.ThrowIfNull(factory); AddHttpClient(services); + services.AddKeyedHttpClient(name); var builder = new DefaultHttpClientBuilder(services, name); builder.AddTypedClient(factory); @@ -785,6 +810,8 @@ public static IHttpClientBuilder AddHttpClient(this IS ThrowHelper.ThrowIfNull(factory); string name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false); + services.AddKeyedHttpClient(name); + return AddHttpClient(services, name, factory); } @@ -823,10 +850,29 @@ public static IHttpClientBuilder AddHttpClient(this IS ThrowHelper.ThrowIfNull(factory); AddHttpClient(services); + services.AddKeyedHttpClient(name); var builder = new DefaultHttpClientBuilder(services, name); builder.AddTypedClient(factory); return builder; } + + private static IServiceCollection AddKeyedHttpClient(this IServiceCollection services, string name) + { + services.AddKeyedTransient(name, KeyedCreateClient) + .AddKeyedTransient(name, KeyedCreateHandler); + + return services; + } + + private static HttpClient KeyedCreateClient(IServiceProvider services, object? key) + { + return services.GetRequiredService().CreateClient((string?)key ?? string.Empty); + } + + private static HttpMessageHandler KeyedCreateHandler(IServiceProvider services, object? key) + { + return services.GetRequiredService().CreateHandler((string?)key ?? string.Empty); + } } } diff --git a/src/libraries/Microsoft.Extensions.Http/src/HttpClientFactoryOptions.cs b/src/libraries/Microsoft.Extensions.Http/src/HttpClientFactoryOptions.cs index ec82a6ca5e44b3..74c81e01eb75ff 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/HttpClientFactoryOptions.cs +++ b/src/libraries/Microsoft.Extensions.Http/src/HttpClientFactoryOptions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Net.Http; using System.Threading; using Microsoft.Extensions.DependencyInjection; @@ -16,7 +17,7 @@ public class HttpClientFactoryOptions { // Establishing a minimum lifetime helps us avoid some possible destructive cases. // - // IMPORTANT: This is used in a resource string. Update the resource if this changes. + // IMPORTANT: MinimumHandlerLifetime is used in a resource string. Update the resource if this changes. internal static readonly TimeSpan MinimumHandlerLifetime = TimeSpan.FromSeconds(1); private TimeSpan _handlerLifetime = TimeSpan.FromMinutes(2); @@ -24,7 +25,12 @@ public class HttpClientFactoryOptions /// /// Gets a list of operations used to configure an . /// - public IList> HttpMessageHandlerBuilderActions { get; } = new List>(); + /// + /// 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. + /// + public IList> HttpMessageHandlerBuilderActions => _httpMessageHandlerBuilderActions ??= MergeToHandlerBuilderActions(); /// /// Gets a list of operations used to configure an . @@ -103,5 +109,58 @@ public TimeSpan HandlerLifetime internal bool SuppressDefaultLogging { get; set; } internal List> LoggingBuilderActions { get; } = new List>(); + + private List>? _httpMessageHandlerBuilderActions; + private List>? _primaryHandlerActions = new List>(); + private List>? _additionalHandlersActions = new List>(); + + internal IReadOnlyList> PrimaryHandlerActions => _primaryHandlerActions!; + internal IReadOnlyList> AdditionalHandlersActions => _additionalHandlersActions!; + internal bool MergedToHandlerBuilderActions => _primaryHandlerActions is null; + + // fallback for backwards compatibility + private List> MergeToHandlerBuilderActions() + { + Debug.Assert(_httpMessageHandlerBuilderActions is null); + Debug.Assert(_primaryHandlerActions is not null); + Debug.Assert(_additionalHandlersActions is not null); + + List> handlerBuilderActions = new List>(); + handlerBuilderActions.AddRange(_primaryHandlerActions); + handlerBuilderActions.AddRange(_additionalHandlersActions); + + _primaryHandlerActions = null; + _additionalHandlersActions = null; + + return handlerBuilderActions; + } + + internal void AddPrimaryHandlerAction(Action action, bool updatesExistingHandler) + { + if (!MergedToHandlerBuilderActions) + { + if (!updatesExistingHandler) + { + _primaryHandlerActions!.Clear(); + } + _primaryHandlerActions!.Add(action); + } + else + { + _httpMessageHandlerBuilderActions!.Add(action); + } + } + + internal void AddAdditionalHandlersAction(Action action) + { + if (!MergedToHandlerBuilderActions) + { + _additionalHandlersActions!.Add(action); + } + else + { + _httpMessageHandlerBuilderActions!.Add(action); + } + } } } diff --git a/src/libraries/Microsoft.Extensions.Http/src/HttpMessageHandlerBuilder.cs b/src/libraries/Microsoft.Extensions.Http/src/HttpMessageHandlerBuilder.cs index da046ecc357438..90c123064e5ef2 100644 --- a/src/libraries/Microsoft.Extensions.Http/src/HttpMessageHandlerBuilder.cs +++ b/src/libraries/Microsoft.Extensions.Http/src/HttpMessageHandlerBuilder.cs @@ -17,7 +17,7 @@ namespace Microsoft.Extensions.Http /// a transient service. Callers should retrieve a new instance for each to /// be created. Implementors should expect each instance to be used a single time. /// - public abstract class HttpMessageHandlerBuilder + public abstract class HttpMessageHandlerBuilder : IPrimaryHandlerBuilder, IAdditionalHandlersBuilder { /// /// Gets or sets the name of the being created. @@ -118,4 +118,21 @@ protected internal static HttpMessageHandler CreateHandlerPipeline(HttpMessageHa return next; } } + + internal interface IHandlerBuilder + { + [DisallowNull] + string? Name { get; set; } + IServiceProvider Services { get; } + } + + internal interface IPrimaryHandlerBuilder : IHandlerBuilder + { + HttpMessageHandler PrimaryHandler { get; set; } + } + + internal interface IAdditionalHandlersBuilder : IHandlerBuilder + { + IList AdditionalHandlers { get; } + } } diff --git a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DefaultHttpClientFactoryTest.cs b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DefaultHttpClientFactoryTest.cs index 1b1b8b305e4c8c..7b2fdf30c285ef 100644 --- a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DefaultHttpClientFactoryTest.cs +++ b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DefaultHttpClientFactoryTest.cs @@ -510,6 +510,102 @@ private async Task SimulateClientUse_Factory_Cleanu return cleanupEntry; } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void HandlerBuilderActionsOptimizationTest(bool accessHandlerBuilderActions) + { + var primaryHandler1 = new TestMessageHandler(); + var primaryHandler2 = new TestMessageHandler(); + + bool firstCreationActionCalled = false; + bool firstUpdateActionCalled = false; + bool secondCreationActionCalled = false; + bool secondUpdateActionCalled = false; + + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddHttpClient("test") + .ConfigurePrimaryHttpMessageHandler(_ => + { + firstCreationActionCalled = true; + return primaryHandler1; + }) + .ConfigurePrimaryHttpMessageHandler((handler, _) => + { + Assert.Same(primaryHandler1, handler); + firstUpdateActionCalled = true; + }) + .AddHttpMessageHandler(_ => new TestDelegatingHandler()); + + serviceCollection.Configure("test", o => + { + Assert.False(o.MergedToHandlerBuilderActions); + Assert.Equal(2, o.PrimaryHandlerActions.Count); + Assert.Equal(1, o.AdditionalHandlersActions.Count); + + if (accessHandlerBuilderActions) + { + // accessing the property will merge the action lists and disable optimizations + Assert.NotNull(o.HttpMessageHandlerBuilderActions); + + Assert.Equal(3, o.HttpMessageHandlerBuilderActions.Count); + Assert.True(o.MergedToHandlerBuilderActions); + Assert.Null(o.PrimaryHandlerActions); + Assert.Null(o.AdditionalHandlersActions); + } + }); + + // continue configuration + serviceCollection.AddHttpClient("test") + .ConfigurePrimaryHttpMessageHandler(_ => + { + secondCreationActionCalled = true; + return primaryHandler2; + }) + .ConfigurePrimaryHttpMessageHandler((handler, _) => + { + Assert.Same(primaryHandler2, handler); + secondUpdateActionCalled = true; + }); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + var options = serviceProvider.GetRequiredService>().Get("test"); + + if (accessHandlerBuilderActions) + { + Assert.True(options.MergedToHandlerBuilderActions); + Assert.Null(options.PrimaryHandlerActions); + Assert.Null(options.AdditionalHandlersActions); + Assert.Equal(5, options.HttpMessageHandlerBuilderActions.Count); + } + else + { + Assert.False(options.MergedToHandlerBuilderActions); + Assert.Equal(2, options.PrimaryHandlerActions.Count); + Assert.Equal(1, options.AdditionalHandlersActions.Count); + } + + var handler = serviceProvider.GetRequiredService().CreateHandler("test"); + while (handler is DelegatingHandler dh) + { + handler = dh.InnerHandler; + } + + Assert.Same(primaryHandler2, handler); // the final handler is the same in both cases + + Assert.Equal(accessHandlerBuilderActions, firstCreationActionCalled); + Assert.Equal(accessHandlerBuilderActions, firstUpdateActionCalled); + Assert.True(secondCreationActionCalled); + Assert.True(secondUpdateActionCalled); + } + + private class TestDelegatingHandler : DelegatingHandler + { + public TestDelegatingHandler() { } + } + private class TestHttpClientFactory : DefaultHttpClientFactory { public TestHttpClientFactory( diff --git a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DefaultHttpMessageHandlerBuilderTest.cs b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DefaultHttpMessageHandlerBuilderTest.cs index 47b206ac326d92..3a6e69efdfb21d 100644 --- a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DefaultHttpMessageHandlerBuilderTest.cs +++ b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DefaultHttpMessageHandlerBuilderTest.cs @@ -21,13 +21,13 @@ public DefaultHttpMessageHandlerBuilderTest() // Testing this because it's an important design detail. If someone wants to globally replace the handler // they can do so by replacing this service. It's important that the Factory isn't the one to instantiate // the handler. The factory has no defaults - it only applies options. - [Fact] + [Fact] public void Ctor_SetsPrimaryHandler() { // Arrange & Act var builder = new DefaultHttpMessageHandlerBuilder(Services); - // Act + // Assert Assert.IsType(builder.PrimaryHandler); } diff --git a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DependencyInjection/HttpClientKeyedRegistrationTest.cs b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DependencyInjection/HttpClientKeyedRegistrationTest.cs new file mode 100644 index 00000000000000..c4cf2fb8faa043 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/DependencyInjection/HttpClientKeyedRegistrationTest.cs @@ -0,0 +1,260 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Http; +using Microsoft.Extensions.Http.Logging; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.Extensions.DependencyInjection +{ + public class HttpClientKeyedRegistrationTest + { + [Fact] + public void HttpClient_ResolvedAsKeyedService_Success() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddHttpClient("test", c => c.BaseAddress = new Uri("http://example.com")); + + var services = serviceCollection.BuildServiceProvider(); + + var client = services.GetRequiredKeyedService("test"); + + Assert.Equal(new Uri("http://example.com"), client.BaseAddress); + } + + [Fact] + public void HttpClient_ResolvedAsKeyedService_AbsentClient() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddHttpClient("test"); + + var services = serviceCollection.BuildServiceProvider(); + + var client = services.GetKeyedService("absent"); + + Assert.Null(client); + } + [Fact] + public void HttpMessageHandler_ResolvedAsKeyedService_Success() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddHttpClient("test") + .ConfigurePrimaryHttpMessageHandler(() => new TestMessageHandler()); + + var services = serviceCollection.BuildServiceProvider(); + + var handler = services.GetRequiredKeyedService("test"); + while (handler is DelegatingHandler dh) + { + handler = dh.InnerHandler; + } + + Assert.IsType(handler); + } + + [Fact] + public void HttpMessageHandler_ResolvedAsKeyedService_AbsentHandler() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddHttpClient("test"); + + var services = serviceCollection.BuildServiceProvider(); + + var handler = services.GetKeyedService("absent"); + + Assert.Null(handler); + } + + [Fact] + public void HttpClient_InjectedAsKeyedService_Success() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddHttpClient("test", c => c.BaseAddress = new Uri("http://example.com")); + serviceCollection.AddSingleton(); + + var services = serviceCollection.BuildServiceProvider(); + + var testService = services.GetRequiredService(); + + Assert.Equal(new Uri("http://example.com"), testService.HttpClient.BaseAddress); + } + + [Fact] + public void AdditionalHandler_InjectedAsKeyedService_Success() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddKeyedTransient(KeyedService.AnyKey); + serviceCollection.AddHttpClient("test") + .AddHttpMessageHandler(); + serviceCollection.AddHttpClient("test2") + .AddHttpMessageHandler(); + + var services = serviceCollection.BuildServiceProvider(); + + ValidateHandler("test"); + ValidateHandler("test2"); + + void ValidateHandler(string clientName) + { + var handler = services.GetRequiredKeyedService(clientName); + while (handler is DelegatingHandler dh) + { + if (dh is KeyedDelegatingHandler) + { + break; + } + handler = dh.InnerHandler; + } + + var keyedHandler = Assert.IsType(handler); + Assert.Equal(clientName, keyedHandler.Key); + } + } + + [Fact] + public void PrimaryHandler_InjectedAsKeyedService_Success() + { + var serviceCollection = new ServiceCollection(); + + serviceCollection.AddKeyedTransient("test"); + serviceCollection.AddKeyedTransient("other-implementation"); + serviceCollection.AddTransient(_ => new KeyedPrimaryHandler("non-keyed-fallback")); + + serviceCollection.AddHttpClient("test") + .ConfigurePrimaryHttpMessageHandler(); + serviceCollection.AddHttpClient("other-implementation") + .ConfigurePrimaryHttpMessageHandler(); + serviceCollection.AddHttpClient("non-keyed") + .ConfigurePrimaryHttpMessageHandler(); + + var services = serviceCollection.BuildServiceProvider(); + + var handler = services.GetRequiredKeyedService("test"); + while (handler is DelegatingHandler dh) + { + handler = dh.InnerHandler; + } + var keyedHandler = Assert.IsType(handler); + Assert.Equal("test", keyedHandler.Key); + + var otherHandler = services.GetRequiredService().CreateHandler("other-implementation"); + while (otherHandler is DelegatingHandler odh) + { + otherHandler = odh.InnerHandler; + } + var otherKeyedHandler = Assert.IsType(otherHandler); + Assert.Equal("{ \"key\": \"other-implementation\" }", otherKeyedHandler.Key); + + var fallbackHandler = services.GetRequiredKeyedService("non-keyed"); + while (fallbackHandler is DelegatingHandler fdh) + { + fallbackHandler = fdh.InnerHandler; + } + var nonKeyedHandler = Assert.IsType(fallbackHandler); + Assert.Equal("non-keyed-fallback", nonKeyedHandler.Key); + } + + [Fact] + public async Task HttpClientLogger_InjectedAsKeyedService_Success() + { + var sink = new TestSink(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddSingleton(new TestLoggerFactory(sink, enabled: true)); + serviceCollection.AddTransient(); + serviceCollection.AddKeyedSingleton(KeyedService.AnyKey); + + serviceCollection.AddHttpClient("FirstClient") + .ConfigurePrimaryHttpMessageHandler() + .RemoveAllLoggers() + .AddLogger(); + + serviceCollection.AddHttpClient("SecondClient") + .ConfigurePrimaryHttpMessageHandler() + .RemoveAllLoggers() + .AddLogger(); + + var services = serviceCollection.BuildServiceProvider(); + var factory = services.GetRequiredService(); + + var client1 = factory.CreateClient("FirstClient"); + var client2 = factory.CreateClient("SecondClient"); + + _ = await client1.GetAsync(new Uri("http://example.com")); + Assert.Equal(2, sink.Writes.Count); + Assert.Equal(2, sink.Writes.Count(w => w.LoggerName == typeof(KeyedHttpClientLogger).FullName + ".FirstClient")); + + _ = await client2.GetAsync(new Uri("http://example.com")); + Assert.Equal(4, sink.Writes.Count); + Assert.Equal(2, sink.Writes.Count(w => w.LoggerName == typeof(KeyedHttpClientLogger).FullName + ".SecondClient")); + } + + internal class KeyedClientTestService + { + public KeyedClientTestService([FromKeyedServices("test")] HttpClient httpClient) + { + HttpClient = httpClient; + } + + public HttpClient HttpClient { get; } + } + + internal class KeyedDelegatingHandler : DelegatingHandler + { + public string Key { get; } + + public KeyedDelegatingHandler([ServiceKey] string key) + { + Key = key; + } + } + + internal abstract class BaseKeyedHandler : TestMessageHandler + { + public string Key { get; protected set; } + } + + internal class KeyedPrimaryHandler : BaseKeyedHandler + { + public KeyedPrimaryHandler([ServiceKey] string key) + { + Key = key; + } + } + + internal class OtherKeyedPrimaryHandler : BaseKeyedHandler + { + public OtherKeyedPrimaryHandler([ServiceKey] string key) + { + Key = "{ \"key\": \"" + key + "\" }"; + } + } + + internal class KeyedHttpClientLogger : IHttpClientLogger + { + private readonly ILogger _logger; + public KeyedHttpClientLogger(ILoggerFactory loggerFactory, [ServiceKey] string key) + { + _logger = loggerFactory.CreateLogger(typeof(KeyedHttpClientLogger).FullName + "." + key); + } + + public object? LogRequestStart(HttpRequestMessage request) + { + _logger.LogInformation("LogRequestStart"); + return null; + } + + public void LogRequestStop(object? context, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed) + => _logger.LogInformation("LogRequestStop"); + + public void LogRequestFailed(object? context, HttpRequestMessage request, HttpResponseMessage? response, Exception exception,TimeSpan elapsed) + => _logger.LogInformation("LogRequestFailed"); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/SocketsHttpHandlerConfigurationTest.cs b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/SocketsHttpHandlerConfigurationTest.cs index 4473917ff728f0..15a21af03cd2ac 100644 --- a/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/SocketsHttpHandlerConfigurationTest.cs +++ b/src/libraries/Microsoft.Extensions.Http/tests/Microsoft.Extensions.Http.Tests/SocketsHttpHandlerConfigurationTest.cs @@ -33,14 +33,14 @@ public void UseSocketsHttpHandler_Parameterless_Success() var serviceCollection = new ServiceCollection(); serviceCollection.AddHttpClient("DefaultPrimaryHandler"); - serviceCollection.AddHttpClient("SocketsHttpHandler") + serviceCollection.AddHttpClient("UseSocketsHttpHandler") .UseSocketsHttpHandler(); var services = serviceCollection.BuildServiceProvider(); var messageHandlerFactory = services.GetRequiredService(); var defaultPrimaryHandlerChain = messageHandlerFactory.CreateHandler("DefaultPrimaryHandler"); - var socketsHttpHandlerChain = messageHandlerFactory.CreateHandler("SocketsHttpHandler"); + var socketsHttpHandlerChain = messageHandlerFactory.CreateHandler("UseSocketsHttpHandler"); Assert.IsType(GetPrimaryHandler(defaultPrimaryHandlerChain)); Assert.IsType(GetPrimaryHandler(socketsHttpHandlerChain));