HttpClient DelegatingHandler 意外的生命周期

HttpClient DelegatingHandler unexpected life cycle

在 ASP.NET Core 2.1 应用程序中,我正在使用 HttpClient 发出 REST 请求。我正在使用 DelegatingHandlers 来定义一些常见的行为。我在这里注册:

private static void AddRestServiceDataClient<TTypedHttpClient, TTypedHttpClientImpl>(this IServiceCollection services)
    where TTypedHttpClient : class
    where TTypedHttpClientImpl : RestServiceDataClient, TTypedHttpClient
{
    var httpClientBuilder = services
        .AddHttpClient<TTypedHttpClient, TTypedHttpClientImpl>()
        .AddHttpMessageHandler<CachingHttpDelegatingHandler>()
        .AddHttpMessageHandler<ExceptionHttpDelegatingHandler>()
        .AddHttpMessageHandler<LoggingHttpDelegatingHandler>();
}

...

// EDIT: You should always register DelegatingHandlers as TRANSIENT (read answer for more).
services.AddScoped<ExceptionHttpDelegatingHandler>();
services.AddScoped<CachingHttpDelegatingHandler>();
services.AddScoped<LoggingHttpDelegatingHandler>();

我将 DelegatingHandlers 注册为 Scoped,但在两个不同的范围(请求)中我得到相同的 DelegatingHandler。我的意思是 DelegationgHandler 的构造函数只被调用一次,并且同一实例用于更多请求(如单例)。其他一切都符合预期 - 其他服务、TypedHttpClients 和 HttpClient 的生命周期都正常。

我在构造函数中通过断点测试了所有内容,并且在每个实例中都测试了 Guid,因此我可以区分实例。

当我将 DelegatingHandlers 注册为 Transient 时,它没有任何区别。

TL;DR DelegatingHandlers 像单例一样解析,即使它们被注册为作用域。这让我的服务生活方式变得混乱。

经过一些调查,我发现尽管 DelegatingHandlers 是通过依赖注入解决的,但是 DelegatingHandlers 的生命周期有点出乎意料,需要对 HttpClientFactory 有更深入的了解.NET 核心 2.1.

HttpClientFactory 每次都会创建新的 HttpClient,但会在多个 HttpClient 之间共享 HttpMessageHandler。您可以在 Steve Gordon's article.

中找到更多相关信息

因为 DelegatingHandler 的实际实例保存在 HttpMessageHandler 中(在 InnerHandler 属性 中递归)并且 HttpMessageHandler 是共享的,那么 DelegatingHandlers 与共享 HttpMessageHandler.

以相同的方式共享并具有相同的生命周期

此处的服务提供商仅用于在 HttpClientFactory "decides to" 时创建新的 DelegatingHandler - 因此每个 DelegatingHandler 都必须注册为瞬态!否则你会得到不确定的行为。 HttpClientFactory 会尝试重用已经使用过的 DelegatingHandler.

解决方法

如果你需要在DelegatingHandler中解析依赖,你可以在构造函数中解析IHttpContextAccessor,然后在httpContextAccessor.HttpContext.RequestServices中通过ServiceProvider解析依赖。

这种方法不完全是 "architecturally clean",但它是我找到的唯一解决方法。

示例:

internal class MyDelegatingHandler : DelegatingHandler
{
    private readonly IHttpContextAccessor httpContextAccessor;

    protected MyDelegatingHandler(IHttpContextAccessor httpContextAccessor)
    {
        this.httpContextAccessor = httpContextAccessor;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var serviceProvider = this.httpContextAccessor.HttpContext.RequestServices;
        var myService = serviceProvider.GetRequiredService<IMyService>();
        ...
    }
}

从 .Net Core 2.2 开始,有一种替代方法,无需使用 IHttpContextAccessor 作为服务定位器来解析范围内的依赖关系。查看此问题的详细信息 here

这对 .NET Core 控制台应用程序最有用:

// configure a client pipeline with the name "MyTypedClient"
...

// then
services.AddTransient<MyTypedClient>((s) =>
{
    var factory = s.GetRequiredService<IHttpMessageHandlerFactory>();
    var handler = factory.CreateHandler(nameof(MyTypedClient));

    var otherHandler = s.GetRequiredService<MyOtherHandler>();
    otherHandler.InnerHandler = handler;
    return new MyTypedClient(new HttpClient(otherHandler, disposeHandler: false));
});