.NET 8 中的 ConfigureHttpClientDefaults

.NET 8 中的 ConfigureHttpClientDefaults

Intro

.NET 8 中新增了一个 ConfigureHttpClientDefaults 的 API,我们可以借助这个 API 来配置所有 HttpClient 的默认行为,比如我们的 HttpClient 都需要带上当前的服务信息或者配置 polly policy,就可以只配置一遍,不再需要每次都配置了

Sample

我们先准备几个 HttpDelegatingHandler 来方便后面的测试

file sealed class MyHttpDelegatingHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Console.WriteLine($"Sending request in {nameof(MyHttpDelegatingHandler)}...");
        return base.SendAsync(request, cancellationToken);
    }
}

file sealed class MyHttpDelegatingHandler2 : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Console.WriteLine($"Sending request in {nameof(MyHttpDelegatingHandler2)}...");
        return base.SendAsync(request, cancellationToken);
    }
}
file sealed class MyHttpDelegatingHandler3 : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Console.WriteLine($"Sending request in {nameof(MyHttpDelegatingHandler3)}...");
        return base.SendAsync(request, cancellationToken);
    }
}

测试示例如下:

var services = new ServiceCollection();
services.AddTransient<MyHttpDelegatingHandler>();
services.AddTransient<MyHttpDelegatingHandler2>();

services.ConfigureHttpClientDefaults(x =>
{
    x.AddHttpMessageHandler<MyHttpDelegatingHandler>();
});
services.AddHttpClient("reservation-client", client =>
{
    client.BaseAddress = new Uri("https://reservation.weihanli.xyz");
}).AddHttpMessageHandler<MyHttpDelegatingHandler2>();
services.AddHttpClient("spark-client", client =>
{
    client.BaseAddress = new Uri("https://spark.weihanli.xyz");
});

await using var serviceProvider = services.BuildServiceProvider();
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
await InvokeHelper.TryInvokeAsync(async () =>
{
    var responseText = await httpClientFactory.CreateClient("reservation-client")
        .GetStringAsync("/health");
    Console.WriteLine(responseText);
});
Console.WriteLine();
await InvokeHelper.TryInvokeAsync(() => httpClientFactory.CreateClient("spark-client")
    .GetStringAsync("/"));

输出结果如下:

c6f0ed5b8ed8f2d89f9b19f368ecfb4c.png

output

从输出结果可以看得出来,我们两个 HttpClient 都执行了默认注册的 MyHttpDelegatingHandler

那它是怎么实现的呢?它其实是基于 Options 来实现的,我们来对比一下 AddHttpClientConfigureHttpClientDefaults 的实现

7a1fe5e074b2678c49b8be119d43d250.png

AddHttpClient

6acf1aff657fd4d1f64337cde0a431ec.png

ConfigureHttpClientDefaults

可以看到 ConfigureHttpClientDefaults 中的 HttpClient name 是 null,这正是 Options 模式的一个神奇用法,我们来简化一下:

var services = new ServiceCollection();
        
services.Configure<NumberOption>(null,o =>
{
    o.Num = 2;
});        

services.Configure<NumberOption>(o =>
{
    o.Num++;
});

services.Configure<NumberOption>("h", o =>
{
    o.Num++;
});
using var sp = services.BuildServiceProvider();

Console.WriteLine(sp.GetRequiredService<IOptionsMonitor<NumberOption>>()
    .Get("h").Num);

file sealed class NumberOption
{
    public int Num { get; set; }
}

大家可以测一下输出的结果是什么?

输出的结果是 3 是不是符合你的预期呢?针对一个 named options 的配置,对于 null 和对应 name 的 configure 都会生效,name 不匹配则不会生效,Configure 不带 name 的重载默认 name 是 string.Empty,而不是 null 所以只有第一个和第三个是生效的,HttpClientDefaults 的实现也是类似的

Deep inside

HttpClient 的 DelegatingHandler 类似于 asp.net core 的中间件,是有注册顺序要求的,那么 HttpClientDefaults 的注册顺序会是怎么样的,调用 ConfigureHttpClientDefaults 的顺序不同会怎么样呢,我们来测试一下,在最后在注册一个 ConfigureHttpClientDefaults 试试看

为了比较方便对比,我们来添加一个方法来输出 HttpClientFactoryOptionsConfigure 的服务注册

private static void DumpHttpClientFactoryOptionsConfigureService(this IServiceCollection services)
{
    Console.WriteLine();
    foreach (var configure in services
                 .Where(x => x.ServiceType == typeof(IConfigureOptions<HttpClientFactoryOptions>))
                 .Select(x=> x.ImplementationInstance)
                 .OfType<ConfigureNamedOptions<HttpClientFactoryOptions>>())
    {
        Console.WriteLine($"Configure HttpClientName: {configure.Name}");
    }
    Console.WriteLine();
}

我们的示例变成了下面这样

var services = new ServiceCollection();
services.AddTransient<MyHttpDelegatingHandler>();
services.AddTransient<MyHttpDelegatingHandler2>();
services.AddTransient<MyHttpDelegatingHandler3>();
services.ConfigureHttpClientDefaults(x =>
{
    x.AddHttpMessageHandler<MyHttpDelegatingHandler>();
});
services.AddHttpClient("reservation-client", client =>
{
    client.BaseAddress = new Uri("https://reservation.weihanli.xyz");
}).AddHttpMessageHandler<MyHttpDelegatingHandler2>();
services.AddHttpClient("spark-client", client =>
{
    client.BaseAddress = new Uri("https://spark.weihanli.xyz");
});
services.ConfigureHttpClientDefaults(x =>
{
    x.AddHttpMessageHandler<MyHttpDelegatingHandler3>();
});

services.DumpHttpClientFactoryOptionsConfigureService();

await using var serviceProvider = services.BuildServiceProvider();
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
await InvokeHelper.TryInvokeAsync(async () =>
{
    var responseText = await httpClientFactory.CreateClient("reservation-client")
        .GetStringAsync("/health");
    Console.WriteLine(responseText);
});
Console.WriteLine();
//
await InvokeHelper.TryInvokeAsync(() => httpClientFactory.CreateClient("spark-client")
    .GetStringAsync("/"));

我们在最后增加通过 ConfigureHttpClientDefaults 注册了 MyHttpDelegatingHandler3,输出结果如下:

78d6a5b48b2c5867b381e9a08fa0fe98.png

从输出结果来看,我们的 Default 配置是在所有 named HttpClient 之前的,并且通过 ConfigureHttpClientDefaults 注册的 handler 也是优先执行的

那这个是因为我们先注册了一个 ConfigureHttpClientDefaults 导致的吗?那我们再来试一个先注册 named HttpClient 的示例看看

var services = new ServiceCollection();
services.AddTransient<MyHttpDelegatingHandler>();
services.AddTransient<MyHttpDelegatingHandler2>();
services.AddTransient<MyHttpDelegatingHandler3>();

services.AddHttpClient("spark-client", client =>
{
    client.BaseAddress = new Uri("https://spark.weihanli.xyz");
}).AddHttpMessageHandler<MyHttpDelegatingHandler2>();

services.ConfigureHttpClientDefaults(x =>
{
    x.AddHttpMessageHandler<MyHttpDelegatingHandler>();
    x.AddHttpMessageHandler<MyHttpDelegatingHandler3>();
});

services.AddHttpClient("reservation-client", client =>
{
    client.BaseAddress = new Uri("https://reservation.weihanli.xyz");
}).AddHttpMessageHandler<MyHttpDelegatingHandler2>();

services.DumpHttpClientFactoryOptionsConfigureService();

await using var serviceProvider = services.BuildServiceProvider();
var httpClientFactory = serviceProvider.GetRequiredService<IHttpClientFactory>();
await InvokeHelper.TryInvokeAsync(async () =>
{
    var responseText = await httpClientFactory.CreateClient("reservation-client")
        .GetStringAsync("/health");
    Console.WriteLine(responseText);
});
Console.WriteLine();
//
await InvokeHelper.TryInvokeAsync(() => httpClientFactory.CreateClient("spark-client")
    .GetStringAsync("/"));

此时的输出结果如下:

aec74431e56c27c85fbedd493d6d26cd.png

可以看到我们 Defaults 的注册还是在最前面的,handler 也是先执行的

那么它是怎么实现的呢?我们可以从源码里学习一下,AddHttpClient 的服务注册里新增了一个 DefaultHttpClientConfigurationTracker

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

实现非常简单,只有一个属性,定义如下:

internal sealed class DefaultHttpClientConfigurationTracker
{
    public ServiceDescriptor? InsertDefaultsAfterDescriptor { get; set; }
}

DefaultHttpClientBuilder 定义如下:

internal sealed class DefaultHttpClientBuilder : IHttpClientBuilder
{
    public DefaultHttpClientBuilder(IServiceCollection services, string 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; }

    public IServiceCollection Services { get; }
}

相比之前的实现,我们现在实现了一个 DefaultHttpClientBuilderServiceCollection,自定义了一个 IServiceCollection 的实现,实现如下,主要在于 Add 的实现:

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);
        }
    }
    // ...
}

可以看到这里会记录下来 Default 的位置,并且会根据需要调整 IConfigureOptions<HttpClientFactoryOptions> 服务注册的位置,借助于此来实现 Default 的配置会优先注册并执行

More

这一实现可以大大方便我们需要针对所有 HttpClient 进行的配置,但是如果你的 Handler 配置有严格的注册顺序,需要考虑和测试一下是不是能够符合预期

对于顺序没有要求,非常通用的 handler 可以考虑通过 ConfigureHttpClientDefaults 这一实现来简化

更多细节可以参考实现的 PR:https://github.com/dotnet/runtime/pull/87953

References

  • https://github.com/dotnet/runtime/issues/87914

  • https://github.com/dotnet/runtime/pull/87953

  • https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/Microsoft.Extensions.Http/src/DependencyInjection/DefaultHttpClientBuilderServiceCollection.cs

  • https://github.com/WeihanLi/SamplesInPractice/blob/main/net8sample/Net8Sample/HttpClientSample.cs

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值