Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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