admin管理员组

文章数量:1033955

基于 keyed DI 的 HttpClient

基于 keyed DI 的 HttpClient

Intro

.NET 8 中依赖注入引入了 keyed service 的支持 可以参考 .NET 8 中的 KeyedService,.NET 9 中改进了 HttpClient 基于名称的 HttpClient 依赖注入,使用基于名称的 HttpClient 的时候可以直接使用 keyed service 来解析了

Sample

我们可以在 AddHttpClient 之后使用 AddAsKeyed() 方法来注册 keyed service

使用示例如下:

代码语言:javascript代码运行次数:0运行复制
var services = new ServiceCollection();
        
services.AddHttpClient("test1", client =>
{
    client.BaseAddress = new Uri("http://localhost:6000");
})
    .AddAsKeyed()
    ;
await using var provider = services.BuildServiceProvider();

var client1 = provider.GetRequiredKeyedService<HttpClient>("test1");
Console.WriteLine(client1.BaseAddress);

注册之后我们就可以从依赖注入容器根据名字获取 HttpClient 服务了如 provider.GetRequiredKeyedService<HttpClient>("test1") 而在之前我们需要使用

代码语言:javascript代码运行次数:0运行复制
scope.ServiceProvider.GetRequiredService<IHttpClientFactory>()
                .CreateClient("test1")

使用 keyed service 之后就可以简化一些了,在 asp core 还可以在 API action 方法上使用 [FromKeyedService("test1")HttpClient client] 的方式来使用

代码语言:javascript代码运行次数:0运行复制
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient("github", c =>
    {
        c.BaseAddress = new Uri("/");
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
        c.DefaultRequestHeaders.Add("User-Agent", "dotnet");
    })
    .AddAsKeyed(); // Add HttpClient as a Keyed Scoped service for key="github"

var app = builder.Build();

// Directly inject the Keyed HttpClient by its name
app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) =>
    httpClient.GetFromJsonAsync<Repo>("/repos/dotnet/runtime"));

app.Run();

record Repo(string Name, string Url);

默认注册的 HttpClient 服务声明周期为 Scoped ,如果要调整可以

代码语言:javascript代码运行次数:0运行复制
public static IHttpClientBuilder AddAsKeyed(this IHttpClientBuilder builder,
      ServiceLifetime lifetime = ServiceLifetime.Scoped)

示例代码如下:

代码语言:javascript代码运行次数:0运行复制
var services = new ServiceCollection();
services.AddHttpClient("test1", client =>
{
    client.BaseAddress = new Uri("http://localhost:5000");
})
    .AddAsKeyed()
    ;
services.AddHttpClient("test2", client =>
{
    client.BaseAddress = new Uri("http://localhost:6000");
})
    .AddAsKeyed(ServiceLifetime.Singleton)
    ;
awaitusingvar provider = services.BuildServiceProvider();
{
    awaitusingvar scope = provider.CreateAsyncScope();

    var client1 = scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("test1");
    Console.WriteLine(client1.GetHashCode());
    Console.WriteLine(client1.BaseAddress);

    var client2 = scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("test2");
    Console.WriteLine(client2.GetHashCode());
    Console.WriteLine(client2.BaseAddress);
}
{
    awaitusingvar scope = provider.CreateAsyncScope();

    var client1 = scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("test1");
    Console.WriteLine(client1.GetHashCode());
    Console.WriteLine(client1.BaseAddress);

    var client2 = scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("test2");
    Console.WriteLine(client2.GetHashCode());
    Console.WriteLine(client2.BaseAddress);
}

这里这两个 HttpClient 一个是默认的生命周期一个是指定的 Singleton,然后创建了两个 scope 输出,两个 scope 的 HttpClient 应该不相同,Singleton 则应该相同

输出结果如下:

如果有很多个 HttpClient 注册,为每个 HttpClient 都写一遍 AddAsKey() 也挺繁琐的,我们可以使用 .NET 8 中引入的 ConfigureHttpClientDefaults 来将所有的 HttpClient 都注册为 named HttpClient 就无需每个 HttpClient 都写一下了

代码语言:javascript代码运行次数:0运行复制
services.ConfigureHttpClientDefaults(c =>
{
    c.AddAsKeyed();
});

使用默认 HttpClient 配置和单个 HttpClient 的配置可以同时使用,单个 HttpClient 的配置生命周期和默认的不一致时会使用单个 HttpClient 的配置

当使用没有注册的名称时会返回默认的 HttpClient

代码语言:javascript代码运行次数:0运行复制
await using var scope = provider.CreateAsyncScope();
var httpClient = scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("default");
Console.WriteLine(httpClient.GetHashCode());
Console.WriteLine(httpClient.BaseAddress);

var httpClient2 = scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("default");
Console.WriteLine(httpClient2.GetHashCode());

另外我们可以通过 RemoveAsKeyed() 方法移除 HttpClient 的 keyed service 注册

代码语言:javascript代码运行次数:0运行复制
public static IHttpClientBuilder RemoveAsKeyed(this IHttpClientBuilder builder)

Implement

它的内部是怎么实现的呢,实际在 httpClient 中注册了一个 keyed service,我们可以反编译或者从源代码中看一看

首先看看 AddAsKeyed() 方法

代码语言:javascript代码运行次数:0运行复制
public static IHttpClientBuilder AddAsKeyed(
  this IHttpClientBuilder builder,
  ServiceLifetime lifetime = ServiceLifetime.Scoped)
{
  ThrowHelper.ThrowIfNull((object) builder, nameof (builder));
string name = builder.Name;
  IServiceCollection services = builder.Services;
  HttpClientMappingRegistry mappingRegistry = services.GetMappingRegistry();
if (name == null)
  {
    mappingRegistry.DefaultKeyedLifetime?.RemoveRegistration(services);
    mappingRegistry.DefaultKeyedLifetime = new HttpClientKeyedLifetime(lifetime);
    mappingRegistry.DefaultKeyedLifetime.AddRegistration(services);
  }
else
  {
    HttpClientKeyedLifetime clientKeyedLifetime1;
    if (mappingRegistry.KeyedLifetimeMap.TryGetValue(name, out clientKeyedLifetime1))
      clientKeyedLifetime1.RemoveRegistration(services);
    HttpClientKeyedLifetime clientKeyedLifetime2 = new HttpClientKeyedLifetime(name, lifetime);
    mappingRegistry.KeyedLifetimeMap[name] = clientKeyedLifetime2;
    clientKeyedLifetime2.AddRegistration(services);
  }
return builder;
}

HttpClientMapingRegistry 是一个 mapping 关系和默认的 HttpClient 的生命周期

代码语言:javascript代码运行次数:0运行复制
internal sealed class HttpClientMappingRegistry
{
    public Dictionary<string, Type> NamedClientRegistrations { get; } = new();

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

    public HttpClientKeyedLifetime? DefaultKeyedLifetime { get; set; }
}

HttpClientKeyedLifetime 实现如下:

代码语言:javascript代码运行次数:0运行复制
internal classHttpClientKeyedLifetime
{
    publicstaticreadonly HttpClientKeyedLifetime Disabled = new(null!, null!, null!);

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

    publicbool 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))
        {
            returnnull!;
        }
        return serviceProvider.GetRequiredService<IHttpClientFactory>().CreateClient(name);
    }

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

可以看到通过 HttpClientKeyedLifetime 来在 service 中注册或者移除 keyed service 的

当在 ConfigureHttpClientDefaults 注册 AddAsKeyed() 时会使用 KeyedService.AnyKey 来注册 HttpClient

不使用 ConfigureHttpClientDefaults 注册 AddAsKeyed() 时根据名称获取不到时就会报错会得到类似下面这样的报错

那么注册了默认的 AddAsKeyed 又移除了单个 HttpClient 的服务会怎么样呢,感兴趣的朋友可以自己尝试一下哈~

References

  • .cs
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-03-28,如有侵权请联系 cloudcommunity@tencent 删除httpclient服务配置生命周期依赖注入

基于 keyed DI 的 HttpClient

基于 keyed DI 的 HttpClient

Intro

.NET 8 中依赖注入引入了 keyed service 的支持 可以参考 .NET 8 中的 KeyedService,.NET 9 中改进了 HttpClient 基于名称的 HttpClient 依赖注入,使用基于名称的 HttpClient 的时候可以直接使用 keyed service 来解析了

Sample

我们可以在 AddHttpClient 之后使用 AddAsKeyed() 方法来注册 keyed service

使用示例如下:

代码语言:javascript代码运行次数:0运行复制
var services = new ServiceCollection();
        
services.AddHttpClient("test1", client =>
{
    client.BaseAddress = new Uri("http://localhost:6000");
})
    .AddAsKeyed()
    ;
await using var provider = services.BuildServiceProvider();

var client1 = provider.GetRequiredKeyedService<HttpClient>("test1");
Console.WriteLine(client1.BaseAddress);

注册之后我们就可以从依赖注入容器根据名字获取 HttpClient 服务了如 provider.GetRequiredKeyedService<HttpClient>("test1") 而在之前我们需要使用

代码语言:javascript代码运行次数:0运行复制
scope.ServiceProvider.GetRequiredService<IHttpClientFactory>()
                .CreateClient("test1")

使用 keyed service 之后就可以简化一些了,在 asp core 还可以在 API action 方法上使用 [FromKeyedService("test1")HttpClient client] 的方式来使用

代码语言:javascript代码运行次数:0运行复制
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient("github", c =>
    {
        c.BaseAddress = new Uri("/");
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
        c.DefaultRequestHeaders.Add("User-Agent", "dotnet");
    })
    .AddAsKeyed(); // Add HttpClient as a Keyed Scoped service for key="github"

var app = builder.Build();

// Directly inject the Keyed HttpClient by its name
app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) =>
    httpClient.GetFromJsonAsync<Repo>("/repos/dotnet/runtime"));

app.Run();

record Repo(string Name, string Url);

默认注册的 HttpClient 服务声明周期为 Scoped ,如果要调整可以

代码语言:javascript代码运行次数:0运行复制
public static IHttpClientBuilder AddAsKeyed(this IHttpClientBuilder builder,
      ServiceLifetime lifetime = ServiceLifetime.Scoped)

示例代码如下:

代码语言:javascript代码运行次数:0运行复制
var services = new ServiceCollection();
services.AddHttpClient("test1", client =>
{
    client.BaseAddress = new Uri("http://localhost:5000");
})
    .AddAsKeyed()
    ;
services.AddHttpClient("test2", client =>
{
    client.BaseAddress = new Uri("http://localhost:6000");
})
    .AddAsKeyed(ServiceLifetime.Singleton)
    ;
awaitusingvar provider = services.BuildServiceProvider();
{
    awaitusingvar scope = provider.CreateAsyncScope();

    var client1 = scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("test1");
    Console.WriteLine(client1.GetHashCode());
    Console.WriteLine(client1.BaseAddress);

    var client2 = scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("test2");
    Console.WriteLine(client2.GetHashCode());
    Console.WriteLine(client2.BaseAddress);
}
{
    awaitusingvar scope = provider.CreateAsyncScope();

    var client1 = scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("test1");
    Console.WriteLine(client1.GetHashCode());
    Console.WriteLine(client1.BaseAddress);

    var client2 = scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("test2");
    Console.WriteLine(client2.GetHashCode());
    Console.WriteLine(client2.BaseAddress);
}

这里这两个 HttpClient 一个是默认的生命周期一个是指定的 Singleton,然后创建了两个 scope 输出,两个 scope 的 HttpClient 应该不相同,Singleton 则应该相同

输出结果如下:

如果有很多个 HttpClient 注册,为每个 HttpClient 都写一遍 AddAsKey() 也挺繁琐的,我们可以使用 .NET 8 中引入的 ConfigureHttpClientDefaults 来将所有的 HttpClient 都注册为 named HttpClient 就无需每个 HttpClient 都写一下了

代码语言:javascript代码运行次数:0运行复制
services.ConfigureHttpClientDefaults(c =>
{
    c.AddAsKeyed();
});

使用默认 HttpClient 配置和单个 HttpClient 的配置可以同时使用,单个 HttpClient 的配置生命周期和默认的不一致时会使用单个 HttpClient 的配置

当使用没有注册的名称时会返回默认的 HttpClient

代码语言:javascript代码运行次数:0运行复制
await using var scope = provider.CreateAsyncScope();
var httpClient = scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("default");
Console.WriteLine(httpClient.GetHashCode());
Console.WriteLine(httpClient.BaseAddress);

var httpClient2 = scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("default");
Console.WriteLine(httpClient2.GetHashCode());

另外我们可以通过 RemoveAsKeyed() 方法移除 HttpClient 的 keyed service 注册

代码语言:javascript代码运行次数:0运行复制
public static IHttpClientBuilder RemoveAsKeyed(this IHttpClientBuilder builder)

Implement

它的内部是怎么实现的呢,实际在 httpClient 中注册了一个 keyed service,我们可以反编译或者从源代码中看一看

首先看看 AddAsKeyed() 方法

代码语言:javascript代码运行次数:0运行复制
public static IHttpClientBuilder AddAsKeyed(
  this IHttpClientBuilder builder,
  ServiceLifetime lifetime = ServiceLifetime.Scoped)
{
  ThrowHelper.ThrowIfNull((object) builder, nameof (builder));
string name = builder.Name;
  IServiceCollection services = builder.Services;
  HttpClientMappingRegistry mappingRegistry = services.GetMappingRegistry();
if (name == null)
  {
    mappingRegistry.DefaultKeyedLifetime?.RemoveRegistration(services);
    mappingRegistry.DefaultKeyedLifetime = new HttpClientKeyedLifetime(lifetime);
    mappingRegistry.DefaultKeyedLifetime.AddRegistration(services);
  }
else
  {
    HttpClientKeyedLifetime clientKeyedLifetime1;
    if (mappingRegistry.KeyedLifetimeMap.TryGetValue(name, out clientKeyedLifetime1))
      clientKeyedLifetime1.RemoveRegistration(services);
    HttpClientKeyedLifetime clientKeyedLifetime2 = new HttpClientKeyedLifetime(name, lifetime);
    mappingRegistry.KeyedLifetimeMap[name] = clientKeyedLifetime2;
    clientKeyedLifetime2.AddRegistration(services);
  }
return builder;
}

HttpClientMapingRegistry 是一个 mapping 关系和默认的 HttpClient 的生命周期

代码语言:javascript代码运行次数:0运行复制
internal sealed class HttpClientMappingRegistry
{
    public Dictionary<string, Type> NamedClientRegistrations { get; } = new();

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

    public HttpClientKeyedLifetime? DefaultKeyedLifetime { get; set; }
}

HttpClientKeyedLifetime 实现如下:

代码语言:javascript代码运行次数:0运行复制
internal classHttpClientKeyedLifetime
{
    publicstaticreadonly HttpClientKeyedLifetime Disabled = new(null!, null!, null!);

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

    publicbool 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))
        {
            returnnull!;
        }
        return serviceProvider.GetRequiredService<IHttpClientFactory>().CreateClient(name);
    }

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

可以看到通过 HttpClientKeyedLifetime 来在 service 中注册或者移除 keyed service 的

当在 ConfigureHttpClientDefaults 注册 AddAsKeyed() 时会使用 KeyedService.AnyKey 来注册 HttpClient

不使用 ConfigureHttpClientDefaults 注册 AddAsKeyed() 时根据名称获取不到时就会报错会得到类似下面这样的报错

那么注册了默认的 AddAsKeyed 又移除了单个 HttpClient 的服务会怎么样呢,感兴趣的朋友可以自己尝试一下哈~

References

  • .cs
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-03-28,如有侵权请联系 cloudcommunity@tencent 删除httpclient服务配置生命周期依赖注入

本文标签: 基于 keyed DI 的 HttpClient