HttpClient DelegatingHandler意外的生命周期

时间:2018-11-09 09:51:59

标签: .net asp.net-core dependency-injection .net-core dotnet-httpclient

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

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>();

我将DelegatingHandler注册为“范围”,但是在两个不同的范围(请求)中,我得到相同的DelegatingHandler。我的意思是DelegationgHandler的构造函数仅被调用一次,并且在更多请求(例如单例)中使用同一实例。其他所有事情都符合预期-其他服务,TypedHttpClients和HttpClient的生命周期都可以。

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

当我将DelegatingHandler s注册为瞬态时,没什么区别。

TL; DR DelegatingHandler像单例一样被解析,即使它们已注册为作用域。这使我沉迷于服务型生活方式。

3 个答案:

答案 0 :(得分:8)

经过一番调查,我发现即使DelegatingHandler s通过可靠注入解决了,DelegatingHandler s的生命周期还是有些意外,需要在.NET中更深入地了解HttpClientFactory核心2.1。

HttpClientFactory每次都创建新的HttpClient,但在多个HttpMessageHandler之间共享HttpClient。您可以在Steve Gordon's article中找到有关此问题的更多信息。

由于DelegatingHandler的实际实例保存在HttpMessageHandler内(递归在InnerHandler属性中),并且HttpMessageHandler被共享,因此DelegatingHandler s被共享与共享的HttpMessageHandler相同的方式并具有相同的生命周期。

服务提供者仅在DelegatingHandler“决定”时用于创建新的HttpClientFactory-因此,每个DelegatingHandler必须注册为瞬态!否则,您将获得不确定的行为。 HttpClientFactory会尝试重用已经使用的DelegatingHandler

解决方法

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

这种方法并非完全“架构上干净”,但这是我发现的唯一解决方法。

示例:

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

答案 1 :(得分:2)

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

答案 2 :(得分:-3)

official documentation指出:

  

在中间件中使用范围服务时,请将服务注入Invoke或InvokeAsync方法。 请勿通过构造函数注入进行注入,因为它会强制服务表现为单例。有关更多信息,请参见ASP.NET Core中间件。

我认为这完全是您的情况