.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("/"));
输出结果如下:
从输出结果可以看得出来,我们两个 HttpClient 都执行了默认注册的 MyHttpDelegatingHandler
那它是怎么实现的呢?它其实是基于 Options
来实现的,我们来对比一下 AddHttpClient
和 ConfigureHttpClientDefaults
的实现
AddHttpClient
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
,输出结果如下:
从输出结果来看,我们的 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("/"));
此时的输出结果如下:
可以看到我们 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