Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Microsoft.Extensions.DependencyInjection
{
public static partial class HttpClientBuilderExtensions
{
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder AddAsKeyed(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, Microsoft.Extensions.DependencyInjection.ServiceLifetime lifetime = Microsoft.Extensions.DependencyInjection.ServiceLifetime.Scoped) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder AddDefaultLogger(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder AddHttpMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Func<System.IServiceProvider, System.Net.Http.DelegatingHandler> configureHandler) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder AddHttpMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.Func<System.Net.Http.DelegatingHandler> configureHandler) { throw null; }
Expand All @@ -27,6 +28,7 @@ public static partial class HttpClientBuilderExtensions
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 RemoveAllLoggers(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder RemoveAsKeyed(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IHttpClientBuilder SetHandlerLifetime(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder builder, System.TimeSpan handlerLifetime) { throw null; }
#if NET
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,11 +369,6 @@ public static IHttpClientBuilder UseSocketsHttpHandler(this IHttpClientBuilder b
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 @@ -650,11 +645,99 @@ public static IHttpClientBuilder ConfigureAdditionalHttpMessageHandlers(this IHt
return builder;
}

public static IHttpClientBuilder AddAsKeyed(this IHttpClientBuilder builder, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{
ThrowHelper.ThrowIfNull(builder);

string? name = builder.Name;
IServiceCollection services = builder.Services;
HttpClientMappingRegistry registry = GetMappingRegistry(services);

UpdateEmptyNameHttpClient(services, registry);

if (name == null)
{
registry.DefaultKeyedLifetime?.RemoveRegistration(services);

registry.DefaultKeyedLifetime = new HttpClientKeyedLifetime(lifetime);
registry.DefaultKeyedLifetime.AddRegistration(services);
}
else
{
if (registry.KeyedLifetimeMap.TryGetValue(name, out HttpClientKeyedLifetime? clientLifetime))
{
clientLifetime.RemoveRegistration(services);
}

clientLifetime = new HttpClientKeyedLifetime(name, lifetime);
registry.KeyedLifetimeMap[name] = clientLifetime;
clientLifetime.AddRegistration(services);
}

return builder;
}

public static IHttpClientBuilder RemoveAsKeyed(this IHttpClientBuilder builder)
{
ThrowHelper.ThrowIfNull(builder);

string? name = builder.Name;
IServiceCollection services = builder.Services;
HttpClientMappingRegistry registry = GetMappingRegistry(services);

UpdateEmptyNameHttpClient(services, registry);

if (name == null)
{
registry.DefaultKeyedLifetime?.RemoveRegistration(services);
registry.DefaultKeyedLifetime = HttpClientKeyedLifetime.Disabled;
}
else
{
if (registry.KeyedLifetimeMap.TryGetValue(name, out HttpClientKeyedLifetime? clientLifetime))
{
clientLifetime.RemoveRegistration(services);
}
registry.KeyedLifetimeMap[name] = HttpClientKeyedLifetime.Disabled;
}

return builder;
}

// workaround for https://github.com/dotnet/runtime/issues/102654
private static void UpdateEmptyNameHttpClient(IServiceCollection services, HttpClientMappingRegistry registry)
{
if (registry.EmptyNameHttpClientDescriptor is not null)
{
bool removed = services.Remove(registry.EmptyNameHttpClientDescriptor);

if (removed)
{
// trying to add it as keyed instead
if (!registry.KeyedLifetimeMap.ContainsKey(string.Empty))
{
var clientLifetime = new HttpClientKeyedLifetime(string.Empty, ServiceLifetime.Transient);
registry.KeyedLifetimeMap[string.Empty] = clientLifetime;
clientLifetime.AddRegistration(services);
}
}
}

if (services.Any(sd => sd.ServiceType == typeof(HttpClient) && sd.ServiceKey is null))
{
throw new InvalidOperationException($"{nameof(AddAsKeyed)} isn't supported when {nameof(HttpClient)} is registered as a service.");
}
}

// See comments on HttpClientMappingRegistry.
private static void ReserveClient(IHttpClientBuilder builder, Type type, string name, bool validateSingleType)
{
var registry = (HttpClientMappingRegistry?)builder.Services.Single(sd => sd.ServiceType == typeof(HttpClientMappingRegistry)).ImplementationInstance;
Debug.Assert(registry != null);
if (builder.Name is null)
{
throw new InvalidOperationException($"{nameof(AddTypedClient)} isn't supported with {nameof(HttpClientFactoryServiceCollectionExtensions.ConfigureHttpClientDefaults)}.");
}

HttpClientMappingRegistry registry = GetMappingRegistry(builder.Services);

// Check for same name registered to two types. This won't work because we rely on named options for the configuration.
if (registry.NamedClientRegistrations.TryGetValue(name, out Type? otherType) &&
Expand All @@ -677,5 +760,8 @@ private static void ReserveClient(IHttpClientBuilder builder, Type type, string
registry.NamedClientRegistrations[name] = type;
}
}

private static HttpClientMappingRegistry GetMappingRegistry(IServiceCollection services)
=> HttpClientFactoryServiceCollectionExtensions.GetMappingRegistry(services);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,7 @@ public static IServiceCollection AddHttpClient(this IServiceCollection services)
services.TryAddSingleton(new DefaultHttpClientConfigurationTracker());

// Register default client as HttpClient
services.TryAddTransient(s =>
{
return s.GetRequiredService<IHttpClientFactory>().CreateClient(string.Empty);
});
TryAddEmptyNameHttpClient(services);

return services;
}
Expand Down Expand Up @@ -834,5 +831,34 @@ public static IHttpClientBuilder AddHttpClient<TClient, TImplementation>(this IS
builder.AddTypedClient<TClient>(factory);
return builder;
}

internal static HttpClientMappingRegistry GetMappingRegistry(IServiceCollection services)
{
var registry = (HttpClientMappingRegistry?)services.Single(sd => sd.ServiceType == typeof(HttpClientMappingRegistry)).ImplementationInstance;
Debug.Assert(registry != null);
return registry;
}

private static void TryAddEmptyNameHttpClient(IServiceCollection services)
{
HttpClientMappingRegistry mappingRegistry = GetMappingRegistry(services);

if (mappingRegistry.EmptyNameHttpClientDescriptor is not null)
{
return;
}

if (services.Any(sd => sd.ServiceType == typeof(HttpClient) && sd.ServiceKey is null))
{
return;
}

mappingRegistry.EmptyNameHttpClientDescriptor = ServiceDescriptor.Transient(s =>
{
return s.GetRequiredService<IHttpClientFactory>().CreateClient(string.Empty);
});

services.Add(mappingRegistry.EmptyNameHttpClientDescriptor);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// 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.Net.Http;
using Microsoft.Extensions.Http;

namespace Microsoft.Extensions.DependencyInjection
{
internal class HttpClientKeyedLifetime
{
public static readonly HttpClientKeyedLifetime Disabled = new(null!, null!, null!);

public object ServiceKey { get; }
public ServiceDescriptor Client { get; }
public ServiceDescriptor Handler { get; }

public bool IsDisabled => ReferenceEquals(this, Disabled);

private HttpClientKeyedLifetime(object serviceKey, ServiceDescriptor client, ServiceDescriptor handler)
{
ServiceKey = serviceKey;
Client = client;
Handler = handler;
}

private HttpClientKeyedLifetime(object serviceKey, ServiceLifetime lifetime)
{
ThrowHelper.ThrowIfNull(serviceKey);
ServiceKey = serviceKey;
Client = ServiceDescriptor.DescribeKeyed(typeof(HttpClient), ServiceKey, CreateKeyedClient, lifetime);
Handler = ServiceDescriptor.DescribeKeyed(typeof(HttpMessageHandler), ServiceKey, CreateKeyedHandler, lifetime);
}

public HttpClientKeyedLifetime(ServiceLifetime lifetime) : this(KeyedService.AnyKey, lifetime) { }
public HttpClientKeyedLifetime(string name, ServiceLifetime lifetime) : this((object)name, lifetime) { }

public void AddRegistration(IServiceCollection services)
{
if (IsDisabled)
{
return;
}

services.Add(Client);
services.Add(Handler);
}

public void RemoveRegistration(IServiceCollection services)
{
if (IsDisabled)
{
return;
}

services.Remove(Client);
services.Remove(Handler);
}

private static HttpClient CreateKeyedClient(IServiceProvider serviceProvider, object? key)
{
if (key is not string name || IsKeyedLifetimeDisabled(serviceProvider, name))
{
return null!;
}
return serviceProvider.GetRequiredService<IHttpClientFactory>().CreateClient(name);
}

private static HttpMessageHandler CreateKeyedHandler(IServiceProvider serviceProvider, object? key)
{
if (key is not string name || IsKeyedLifetimeDisabled(serviceProvider, name))
{
return null!;
}
HttpMessageHandler handler = serviceProvider.GetRequiredService<IHttpMessageHandlerFactory>().CreateHandler(name);
// factory will return a cached instance, wrap it to be able to respect DI lifetimes
return new LifetimeTrackingHttpMessageHandler(handler);
}

private static bool IsKeyedLifetimeDisabled(IServiceProvider serviceProvider, string name)
{
HttpClientMappingRegistry registry = serviceProvider.GetRequiredService<HttpClientMappingRegistry>();

if (!registry.KeyedLifetimeMap.TryGetValue(name, out HttpClientKeyedLifetime? registration))
{
registration = registry.DefaultKeyedLifetime;
}

return registration?.IsDisabled ?? false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ namespace Microsoft.Extensions.DependencyInjection
// See: https://github.com/dotnet/extensions/issues/960
internal sealed class HttpClientMappingRegistry
{
public Dictionary<string, Type> NamedClientRegistrations { get; } = new Dictionary<string, Type>();
public Dictionary<string, Type> NamedClientRegistrations { get; } = new();

public Dictionary<string, HttpClientKeyedLifetime> KeyedLifetimeMap { get; } = new();

public HttpClientKeyedLifetime? DefaultKeyedLifetime { get; set; }

public ServiceDescriptor? EmptyNameHttpClientDescriptor { get; set; }
}
}
Loading