Skip to content

Commit

Permalink
Add AddHttpClientDefaults (#87953)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
JamesNK and CarnaViire committed Jul 14, 2023
1 parent 1318299 commit 7980417
Show file tree
Hide file tree
Showing 8 changed files with 406 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ public static partial class HttpClientBuilderExtensions
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; }
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; }
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; }
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; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigureHttpClient(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Action<System.IServiceProvider, System.Net.Http.HttpClient> configureClient) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigureHttpClient(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Action<System.Net.Http.HttpClient> configureClient) { throw null; }
[System.Obsolete("This method has been deprecated. Use ConfigurePrimaryHttpMessageHandler or ConfigureAdditionalHttpMessageHandlers instead.")]
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigureHttpMessageHandlerBuilder(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Action<Microsoft.Extensions.Http.HttpMessageHandlerBuilder> configureBuilder) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Func<System.IServiceProvider, System.Net.Http.HttpMessageHandler> configureHandler) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Func<System.Net.Http.HttpMessageHandler> configureHandler) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigurePrimaryHttpMessageHandler<THandler>(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder) where THandler : System.Net.Http.HttpMessageHandler { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder ConfigurePrimaryHttpMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Action<System.Net.Http.HttpMessageHandler, System.IServiceProvider> configureHandler) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder RedactLoggedHeaders(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Collections.Generic.IEnumerable<string> redactedLoggedHeaderNames) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder RedactLoggedHeaders(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Func<string, bool> shouldRedactHeaderValue) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder SetHandlerLifetime(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.TimeSpan handlerLifetime) { throw null; }
Expand All @@ -44,6 +47,7 @@ public static partial class HttpClientFactoryServiceCollectionExtensions
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; }
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; }
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; }
public static Microsoft.Extensions.DependencyInjection.IServiceCollection ConfigureHttpClientDefaults(Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<Microsoft.Extensions.DependencyInjection.IHttpClientBuilder> configure) { throw null; }
}
public partial interface IHttpClientBuilder
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Linq;

namespace Microsoft.Extensions.DependencyInjection
{
internal sealed class DefaultHttpClientBuilder : IHttpClientBuilder
{
public DefaultHttpClientBuilder(IServiceCollection services, string name)
{
Services = services;
Name = name;
// The tracker references a descriptor. It marks the position of where default services are added to the collection.
var tracker = (DefaultHttpClientConfigurationTracker?)services.Single(sd => sd.ServiceType == typeof(DefaultHttpClientConfigurationTracker)).ImplementationInstance;
Debug.Assert(tracker != null);

Services = new DefaultHttpClientBuilderServiceCollection(services, name == null, tracker);
Name = name!;
}

public string Name { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.DependencyInjection
{
internal sealed class DefaultHttpClientBuilderServiceCollection : IServiceCollection
{
private readonly IServiceCollection _services;
private readonly bool _isDefault;
private readonly DefaultHttpClientConfigurationTracker _tracker;

public DefaultHttpClientBuilderServiceCollection(IServiceCollection services, bool isDefault, DefaultHttpClientConfigurationTracker tracker)
{
_services = services;
_isDefault = isDefault;
_tracker = tracker;
}

public void Add(ServiceDescriptor item)
{
if (item.ServiceType != typeof(IConfigureOptions<HttpClientFactoryOptions>))
{
_services.Add(item);
return;
}

if (_isDefault)
{
// Insert IConfigureOptions<HttpClientFactoryOptions> services into the collection before named config descriptors.
// This ensures they run and apply configuration first. Configuration for named clients run afterwards.
if (_tracker.InsertDefaultsAfterDescriptor != null &&
_services.IndexOf(_tracker.InsertDefaultsAfterDescriptor) is var index && index != -1)
{
index++;
_services.Insert(index, item);
}
else
{
_services.Add(item);
}

_tracker.InsertDefaultsAfterDescriptor = item;
}
else
{
// Track the location of where the first named config descriptor was added.
_tracker.InsertDefaultsAfterDescriptor ??= _services.Last();

_services.Add(item);
}
}

public ServiceDescriptor this[int index]
{
get => _services[index];
set => _services[index] = value;
}
public int Count => _services.Count;
public bool IsReadOnly => _services.IsReadOnly;
public void Clear() => _services.Clear();
public bool Contains(ServiceDescriptor item) => _services.Contains(item);
public void CopyTo(ServiceDescriptor[] array, int arrayIndex) => _services.CopyTo(array, arrayIndex);
public IEnumerator<ServiceDescriptor> GetEnumerator() => _services.GetEnumerator();
public int IndexOf(ServiceDescriptor item) => _services.IndexOf(item);
public void Insert(int index, ServiceDescriptor item) => _services.Insert(index, item);
public bool Remove(ServiceDescriptor item) => _services.Remove(item);
public void RemoveAt(int index) => _services.RemoveAt(index);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Extensions.DependencyInjection
{
internal sealed class DefaultHttpClientConfigurationTracker
{
public ServiceDescriptor? InsertDefaultsAfterDescriptor { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,40 @@ public static IHttpClientBuilder ConfigurePrimaryHttpMessageHandler<THandler>(th
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 configure a previously set or default primary <see cref="HttpMessageHandler"/>.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
/// <remarks>
/// <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, Action<HttpMessageHandler, IServiceProvider> configureHandler)
{
ThrowHelper.ThrowIfNull(builder);
ThrowHelper.ThrowIfNull(configureHandler);

builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>
{
options.HttpMessageHandlerBuilderActions.Add(b => configureHandler(b.PrimaryHandler, b.Services));
});

return builder;
}

/// <summary>
/// Adds a delegate that will be used to configure message handlers using <see cref="HttpMessageHandlerBuilder"/>
/// for a named <see cref="HttpClient"/>.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <param name="configureBuilder">A delegate that is used to configure an <see cref="HttpMessageHandlerBuilder"/>.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
[Obsolete("This method has been deprecated. Use ConfigurePrimaryHttpMessageHandler or ConfigureAdditionalHttpMessageHandlers instead.")]
public static IHttpClientBuilder ConfigureHttpMessageHandlerBuilder(this IHttpClientBuilder builder, Action<HttpMessageHandlerBuilder> configureBuilder)
{
ThrowHelper.ThrowIfNull(builder);
Expand Down Expand Up @@ -275,6 +302,11 @@ public static IHttpClientBuilder ConfigureHttpMessageHandlerBuilder(this IHttpCl
this IHttpClientBuilder builder, bool validateSingleType)
where TClient : class
{
if (builder.Name is null)
{
throw new InvalidOperationException($"{nameof(HttpClientBuilderExtensions.AddTypedClient)} isn't supported with {nameof(HttpClientFactoryServiceCollectionExtensions.ConfigureHttpClientDefaults)}.");
}

ReserveClient(builder, typeof(TClient), builder.Name, validateSingleType);

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

/// <summary>
/// Adds a delegate that will be used to configure additional message handlers using <see cref="HttpMessageHandlerBuilder"/>
/// for a named <see cref="HttpClient"/>.
/// </summary>
/// <param name="builder">The <see cref="IHttpClientBuilder"/>.</param>
/// <param name="configureAdditionalHandlers">A delegate that is used to configure a collection of <see cref="DelegatingHandler"/>s.</param>
/// <returns>An <see cref="IHttpClientBuilder"/> that can be used to configure the client.</returns>
public static IHttpClientBuilder ConfigureAdditionalHttpMessageHandlers(this IHttpClientBuilder builder, Action<IList<DelegatingHandler>, IServiceProvider> configureAdditionalHandlers)
{
ThrowHelper.ThrowIfNull(builder);
ThrowHelper.ThrowIfNull(configureAdditionalHandlers);

builder.Services.Configure<HttpClientFactoryOptions>(builder.Name, options =>
{
options.HttpMessageHandlerBuilderActions.Add(b => configureAdditionalHandlers(b.AdditionalHandlers, b.Services));
});

return builder;
}

// See comments on HttpClientMappingRegistry.
private static void ReserveClient(IHttpClientBuilder builder, Type type, string name, bool validateSingleType)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Http;
Expand Down Expand Up @@ -50,6 +53,9 @@ public static IServiceCollection AddHttpClient(this IServiceCollection services)
// because we access it by reaching into the service collection.
services.TryAddSingleton(new HttpClientMappingRegistry());

// This is used to store configuration for the default builder.
services.TryAddSingleton(new DefaultHttpClientConfigurationTracker());

// Register default client as HttpClient
services.TryAddTransient(s =>
{
Expand All @@ -59,6 +65,24 @@ public static IServiceCollection AddHttpClient(this IServiceCollection services)
return services;
}

/// <summary>
/// Adds a delegate that will be used to configure all <see cref="HttpClient"/> instances.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="configure">A delegate that is used to configure an <see cref="IHttpClientBuilder"/>.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection ConfigureHttpClientDefaults(this IServiceCollection services, Action<IHttpClientBuilder> configure)
{
ThrowHelper.ThrowIfNull(services);
ThrowHelper.ThrowIfNull(configure);

AddHttpClient(services);

configure(new DefaultHttpClientBuilder(services, name: null!));

return services;
}

/// <summary>
/// Adds the <see cref="IHttpClientFactory"/> and related services to the <see cref="IServiceCollection"/> and configures
/// a named <see cref="HttpClient"/>.
Expand Down
Loading

0 comments on commit 7980417

Please sign in to comment.