Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

HttpClient DelegatingHandler unexpected life cycle

In ASP.NET Core 2.1 app I am making REST request using HttpClient. I am using DelegatingHandlers to define some common behavior. The registration i here:

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

I register DelegatingHandlers as Scoped, but in two different scopes (requests) I get the same DelegatingHandler. I mean that the constructor of DelegationgHandler is being called only once and the same instance is used across more requests (like singleton). Everything else is as expected - life cycle of other services, TypedHttpClients and HttpClient is ok.

I tested everything by breakpoint in constructor and I have testing Guid in every instance so I can distinguish instances.

When I register DelegatingHandlers as Transient it makes no difference.

TL;DR DelegatingHandlers are resolved like singleton even though they are registered as scoped. And this causes me mishmash in service lifestyles.

like image 728
Martin Volek Avatar asked Nov 09 '18 09:11

Martin Volek


People also ask

What is Delegatinghandler?

Typically, a series of message handlers are chained together. The first handler receives an HTTP request, does some processing, and gives the request to the next handler. At some point, the response is created and goes back up the chain. This pattern is called a delegating handler.

Is AddHttpClient transient?

In the preceding code, AddHttpClient registers GitHubService as a transient service. This registration uses a factory method to: Create an instance of HttpClient .

What is IHttpClientFactory?

IHttpClientFactory is a contract implemented by DefaultHttpClientFactory , an opinionated factory, available since . NET Core 2.1, for creating HttpClient instances to be used in your applications.

What is ConfigurePrimaryHttpMessageHandler?

ConfigurePrimaryHttpMessageHandler(IHttpClientBuilder, Func<IServiceProvider,HttpMessageHandler>) Adds a delegate that will be used to configure the primary HttpMessageHandler for a named HttpClient.


2 Answers

After some investigation I found out that even though DelegatingHandlers are resolved by dependecy injection, the lifecycle of DelegatingHandlers is a bit unexpected and requires deeper knowledge of HttpClientFactory in .NET Core 2.1.

HttpClientFactory creates new HttpClient every time, but shares HttpMessageHandler across multiple HttpClients. More information about this you can find in Steve Gordon's article.

Because actual instances of DelegatingHandlers are held inside HttpMessageHandler (recursively in InnerHandler property) and HttpMessageHandler is shared, then DelegatingHandlers are shared the same way and have same lifecycle as the shared HttpMessageHandler.

Service provider is here used only for creating new DelegatingHandlers when HttpClientFactory "decides to" - thus every DelegatingHandler must be registered as transient! Otherwise you would get non-deterministic behavior. HttpClientFactory would try to reuse already used DelegatingHandler.

Workaround

If you need to resolve dependencies in DelegatingHandler you can resolve IHttpContextAccessor in constructor and then resolve dependencies by ServiceProvider in httpContextAccessor.HttpContext.RequestServices.

This approach is not exactly "architecturally clean" but it is the only workaround I have found.

Example:

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>();
        ...
    }
}
like image 199
Martin Volek Avatar answered Nov 12 '22 02:11

Martin Volek


Starting with .Net Core 2.2 there is an alternative without using IHttpContextAccessor as a service locator to resolve scoped dependencies. See the details on this issue here.

This is most useful for .NET Core console apps:

// 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));
}); 
like image 37
Sergiu Enache Avatar answered Nov 12 '22 01:11

Sergiu Enache