Skip to content

Commit 7980417

Browse files
JamesNKCarnaViire
andauthored
Add AddHttpClientDefaults (#87953)
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>
1 parent 1318299 commit 7980417

File tree

8 files changed

+406
-13
lines changed

8 files changed

+406
-13
lines changed

src/libraries/Microsoft.Extensions.Http/ref/Microsoft.Extensions.Http.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ public static partial class HttpClientBuilderExtensions
1212
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder AddTypedClient<TClient>(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Func<System.Net.Http.HttpClient, System.IServiceProvider, TClient> factory) where TClient : class { throw null; }
1313
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder AddTypedClient<TClient>(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Func<System.Net.Http.HttpClient, TClient> factory) where TClient : class { throw null; }
1414
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder AddTypedClient<TClient, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder) where TClient : class where TImplementation : class, TClient { throw null; }
15+
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigureAdditionalHttpMessageHandlers(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Action<System.Collections.Generic.IList<System.Net.Http.DelegatingHandler>, System.IServiceProvider> configureAdditionalHandlers) { throw null; }
1516
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigureHttpClient(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Action<System.IServiceProvider, System.Net.Http.HttpClient> configureClient) { throw null; }
1617
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigureHttpClient(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Action<System.Net.Http.HttpClient> configureClient) { throw null; }
18+
[System.Obsolete("This method has been deprecated. Use ConfigurePrimaryHttpMessageHandler or ConfigureAdditionalHttpMessageHandlers instead.")]
1719
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigureHttpMessageHandlerBuilder(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Action<Microsoft.Extensions.Http.HttpMessageHandlerBuilder> configureBuilder) { throw null; }
1820
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Func<System.IServiceProvider, System.Net.Http.HttpMessageHandler> configureHandler) { throw null; }
1921
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Func<System.Net.Http.HttpMessageHandler> configureHandler) { throw null; }
2022
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigurePrimaryHttpMessageHandler<THandler>(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder) where THandler : System.Net.Http.HttpMessageHandler { throw null; }
23+
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Action<System.Net.Http.HttpMessageHandler, System.IServiceProvider> configureHandler) { throw null; }
2124
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder RedactLoggedHeaders(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Collections.Generic.IEnumerable<string> redactedLoggedHeaderNames) { throw null; }
2225
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder RedactLoggedHeaders(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Func<string, bool> shouldRedactHeaderValue) { throw null; }
2326
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder SetHandlerLifetime(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.TimeSpan handlerLifetime) { throw null; }
@@ -44,6 +47,7 @@ public static partial class HttpClientFactoryServiceCollectionExtensions
4447
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder AddHttpClient<TClient, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, string name, System.Action<System.Net.Http.HttpClient> configureClient) where TClient : class where TImplementation : class, TClient { throw null; }
4548
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder AddHttpClient<TClient, TImplementation>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, string name, System.Func<System.Net.Http.HttpClient, System.IServiceProvider, TImplementation> factory) where TClient : class where TImplementation : class, TClient { throw null; }
4649
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder AddHttpClient<TClient, TImplementation>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, string name, System.Func<System.Net.Http.HttpClient, TImplementation> factory) where TClient : class where TImplementation : class, TClient { throw null; }
50+
public static Microsoft.Extensions.DependencyInjection.IServiceCollection ConfigureHttpClientDefaults(Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<Microsoft.Extensions.DependencyInjection.IHttpClientBuilder> configure) { throw null; }
4751
}
4852
public partial interface IHttpClientBuilder
4953
{

src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/DefaultHttpClientBuilder.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
5+
using System.Linq;
6+
47
namespace Microsoft.Extensions.DependencyInjection
58
{
69
internal sealed class DefaultHttpClientBuilder : IHttpClientBuilder
710
{
811
public DefaultHttpClientBuilder(IServiceCollection services, string name)
912
{
10-
Services = services;
11-
Name = name;
13+
// The tracker references a descriptor. It marks the position of where default services are added to the collection.
14+
var tracker = (DefaultHttpClientConfigurationTracker?)services.Single(sd => sd.ServiceType == typeof(DefaultHttpClientConfigurationTracker)).ImplementationInstance;
15+
Debug.Assert(tracker != null);
16+
17+
Services = new DefaultHttpClientBuilderServiceCollection(services, name == null, tracker);
18+
Name = name!;
1219
}
1320

1421
public string Name { get; }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using Microsoft.Extensions.Http;
8+
using Microsoft.Extensions.Options;
9+
10+
namespace Microsoft.Extensions.DependencyInjection
11+
{
12+
internal sealed class DefaultHttpClientBuilderServiceCollection : IServiceCollection
13+
{
14+
private readonly IServiceCollection _services;
15+
private readonly bool _isDefault;
16+
private readonly DefaultHttpClientConfigurationTracker _tracker;
17+
18+
public DefaultHttpClientBuilderServiceCollection(IServiceCollection services, bool isDefault, DefaultHttpClientConfigurationTracker tracker)
19+
{
20+
_services = services;
21+
_isDefault = isDefault;
22+
_tracker = tracker;
23+
}
24+
25+
public void Add(ServiceDescriptor item)
26+
{
27+
if (item.ServiceType != typeof(IConfigureOptions<HttpClientFactoryOptions>))
28+
{
29+
_services.Add(item);
30+
return;
31+
}
32+
33+
if (_isDefault)
34+
{
35+
// Insert IConfigureOptions<HttpClientFactoryOptions> services into the collection before named config descriptors.
36+
// This ensures they run and apply configuration first. Configuration for named clients run afterwards.
37+
if (_tracker.InsertDefaultsAfterDescriptor != null &&
38+
_services.IndexOf(_tracker.InsertDefaultsAfterDescriptor) is var index && index != -1)
39+
{
40+
index++;
41+
_services.Insert(index, item);
42+
}
43+
else
44+
{
45+
_services.Add(item);
46+
}
47+
48+
_tracker.InsertDefaultsAfterDescriptor = item;
49+
}
50+
else
51+
{
52+
// Track the location of where the first named config descriptor was added.
53+
_tracker.InsertDefaultsAfterDescriptor ??= _services.Last();
54+
55+
_services.Add(item);
56+
}
57+
}
58+
59+
public ServiceDescriptor this[int index]
60+
{
61+
get => _services[index];
62+
set => _services[index] = value;
63+
}
64+
public int Count => _services.Count;
65+
public bool IsReadOnly => _services.IsReadOnly;
66+
public void Clear() => _services.Clear();
67+
public bool Contains(ServiceDescriptor item) => _services.Contains(item);
68+
public void CopyTo(ServiceDescriptor[] array, int arrayIndex) => _services.CopyTo(array, arrayIndex);
69+
public IEnumerator<ServiceDescriptor> GetEnumerator() => _services.GetEnumerator();
70+
public int IndexOf(ServiceDescriptor item) => _services.IndexOf(item);
71+
public void Insert(int index, ServiceDescriptor item) => _services.Insert(index, item);
72+
public bool Remove(ServiceDescriptor item) => _services.Remove(item);
73+
public void RemoveAt(int index) => _services.RemoveAt(index);
74+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.Extensions.DependencyInjection
5+
{
6+
internal sealed class DefaultHttpClientConfigurationTracker
7+
{
8+
public ServiceDescriptor? InsertDefaultsAfterDescriptor { get; set; }
9+
}
10+
}

src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientBuilderExtensions.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,40 @@ public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler<THandler>(th
220220
return builder;
221221
}
222222

223+
/// <summary>
224+
/// Adds a delegate that will be used to configure the primary <see cref="HttpMessageHandler"/> for a
225+
/// named <see cref="HttpClient"/>.
226+
/// </summary>
227+
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
228+
/// <param name="configureHandler">A delegate that is used to configure a previously set or default primary <see cref="HttpMessageHandler"/>.</param>
229+
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
230+
/// <remarks>
231+
/// <para>
232+
/// The <see cref="IServiceProvider"/> argument provided to <paramref name="configureHandler"/> will be
233+
/// a reference to a scoped service provider that shares the lifetime of the handler being constructed.
234+
/// </para>
235+
/// </remarks>
236+
public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this IHttpClientBuilder builder, Action<HttpMessageHandler, IServiceProvider> configureHandler)
237+
{
238+
ThrowHelper.ThrowIfNull(builder);
239+
ThrowHelper.ThrowIfNull(configureHandler);
240+
241+
builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>
242+
{
243+
options.HttpMessageHandlerBuilderActions.Add(b => configureHandler(b.PrimaryHandler, b.Services));
244+
});
245+
246+
return builder;
247+
}
248+
223249
/// <summary>
224250
/// Adds a delegate that will be used to configure message handlers using <see cref="HttpMessageHandlerBuilder"/>
225251
/// for a named <see cref="HttpClient"/>.
226252
/// </summary>
227253
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
228254
/// <param name="configureBuilder">A delegate that is used to configure an <see cref="HttpMessageHandlerBuilder"/>.</param>
229255
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
256+
[Obsolete("This method has been deprecated. Use ConfigurePrimaryHttpMessageHandler or ConfigureAdditionalHttpMessageHandlers instead.")]
230257
public static IHttpClientBuilder ConfigureHttpMessageHandlerBuilder(this IHttpClientBuilder builder, Action<HttpMessageHandlerBuilder> configureBuilder)
231258
{
232259
ThrowHelper.ThrowIfNull(builder);
@@ -275,6 +302,11 @@ public static IHttpClientBuilder ConfigureHttpMessageHandlerBuilder(this IHttpCl
275302
this IHttpClientBuilder builder, bool validateSingleType)
276303
where TClient : class
277304
{
305+
if (builder.Name is null)
306+
{
307+
throw new InvalidOperationException($"{nameof(HttpClientBuilderExtensions.AddTypedClient)} isn't supported with {nameof(HttpClientFactoryServiceCollectionExtensions.ConfigureHttpClientDefaults)}.");
308+
}
309+
278310
ReserveClient(builder, typeof(TClient), builder.Name, validateSingleType);
279311

280312
builder.Services.AddTransient(s => AddTransientHelper<TClient>(s, builder));
@@ -531,6 +563,26 @@ public static IHttpClientBuilder SetHandlerLifetime(this IHttpClientBuilder buil
531563
return builder;
532564
}
533565

566+
/// <summary>
567+
/// Adds a delegate that will be used to configure additional message handlers using <see cref="HttpMessageHandlerBuilder"/>
568+
/// for a named <see cref="HttpClient"/>.
569+
/// </summary>
570+
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
571+
/// <param name="configureAdditionalHandlers">A delegate that is used to configure a collection of <see cref="DelegatingHandler"/>s.</param>
572+
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
573+
public static IHttpClientBuilder ConfigureAdditionalHttpMessageHandlers(this IHttpClientBuilder builder, Action<IList<DelegatingHandler>, IServiceProvider> configureAdditionalHandlers)
574+
{
575+
ThrowHelper.ThrowIfNull(builder);
576+
ThrowHelper.ThrowIfNull(configureAdditionalHandlers);
577+
578+
builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>
579+
{
580+
options.HttpMessageHandlerBuilderActions.Add(b => configureAdditionalHandlers(b.AdditionalHandlers, b.Services));
581+
});
582+
583+
return builder;
584+
}
585+
534586
// See comments on HttpClientMappingRegistry.
535587
private static void ReserveClient(IHttpClientBuilder builder, Type type, string name, bool validateSingleType)
536588
{

src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/HttpClientFactoryServiceCollectionExtensions.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
57
using System.Diagnostics.CodeAnalysis;
8+
using System.Linq;
69
using System.Net.Http;
710
using Microsoft.Extensions.DependencyInjection.Extensions;
811
using Microsoft.Extensions.Http;
@@ -50,6 +53,9 @@ public static IServiceCollection AddHttpClient(this IServiceCollection services)
5053
// because we access it by reaching into the service collection.
5154
services.TryAddSingleton(new HttpClientMappingRegistry());
5255

56+
// This is used to store configuration for the default builder.
57+
services.TryAddSingleton(new DefaultHttpClientConfigurationTracker());
58+
5359
// Register default client as HttpClient
5460
services.TryAddTransient(s =>
5561
{
@@ -59,6 +65,24 @@ public static IServiceCollection AddHttpClient(this IServiceCollection services)
5965
return services;
6066
}
6167

68+
/// <summary>
69+
/// Adds a delegate that will be used to configure all <see cref="HttpClient"/> instances.
70+
/// </summary>
71+
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
72+
/// <param name="configure">A delegate that is used to configure an <see cref="IHttpClientBuilder"/>.</param>
73+
/// <returns>The <see cref="IServiceCollection"/>.</returns>
74+
public static IServiceCollection ConfigureHttpClientDefaults(this IServiceCollection services, Action<IHttpClientBuilder> configure)
75+
{
76+
ThrowHelper.ThrowIfNull(services);
77+
ThrowHelper.ThrowIfNull(configure);
78+
79+
AddHttpClient(services);
80+
81+
configure(new DefaultHttpClientBuilder(services, name: null!));
82+
83+
return services;
84+
}
85+
6286
/// <summary>
6387
/// Adds the <see cref="IHttpClientFactory"/> and related services to the <see cref="IServiceCollection"/> and configures
6488
/// a named <see cref="HttpClient"/>.

0 commit comments

Comments
 (0)