In ASP.NET Core 2.1 app I am making REST request using HttpClient
. I am using DelegatingHandler
s 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 DelegatingHandler
s 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 DelegatingHandler
s as Transient it makes no difference.
TL;DR DelegatingHandler
s are resolved like singleton even though they are registered as scoped. And this causes me mishmash in service lifestyles.
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.
In the preceding code, AddHttpClient registers GitHubService as a transient service. This registration uses a factory method to: Create an instance of HttpClient .
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.
ConfigurePrimaryHttpMessageHandler(IHttpClientBuilder, Func<IServiceProvider,HttpMessageHandler>) Adds a delegate that will be used to configure the primary HttpMessageHandler for a named HttpClient.
After some investigation I found out that even though DelegatingHandler
s are resolved by dependecy injection, the lifecycle of DelegatingHandler
s 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 HttpClient
s. More information about this you can find in Steve Gordon's article.
Because actual instances of DelegatingHandler
s are held inside HttpMessageHandler
(recursively in InnerHandler
property) and HttpMessageHandler
is shared, then DelegatingHandler
s are shared the same way and have same lifecycle as the shared HttpMessageHandler
.
Service provider is here used only for creating new DelegatingHandler
s 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>();
...
}
}
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));
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With