We have a dotnet-core 2.1 web-api project setup as following.
We are trying to use MediatR together with a HttpClient and send events for different responses in the HttpMagicService. Problem arrises when trying to use an injected service after a call to HttpClient.SendAsync()
.
Initial setup for IMagicService
and MediatR.
public void ConfigureServices(IServiceCollection services)
{
// other services configured above
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestLoggingBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestPerformanceBehaviour<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestValidationBehavior<,>));
services.AddMediatR(typeof(ApplicaitonCommand).GetTypeInfo().Assembly);
services.AddHttpClient<IMagicService, HttpMagicService>();
}
Implementation of IMagicService
.
public class HttpMagicService : IMagicService
{
private readonly IMediator mediator;
private readonly HttpClient httpClient;
public HttpMagicService(IMediator mediator, HttpClient httpClient)
{
this.mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public async Task AddAsync(Magic data, Metadata metadata)
{
// This one works
await this.mediator.Publish(new MagicAddInitIntegrationEvent(data));
var url = metadata.RemoteUri + "/command/AddMagic";
using (var request = new HttpRequestMessage(HttpMethod.Post, url))
{
try
{
var httpContent = CreateHttpContent(data);
request.Content = httpContent;
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", metadata.AuthKey);
// httpClient clears the ResolvedServices?!
var response = await this.httpClient.SendAsync(request).ConfigureAwait(false);
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
// This one fails
await this.mediator.Publish(new MagicAddIntegrationEvent(data));
}
else
{
await this.mediator.Publish(new MagicAddFailedIntegrationEvent(data));
}
}
catch (Exception ex)
{
await this.mediator.Publish(new MagicAddFailedIntegrationEvent(meeting, ex));
}
}
}
private static HttpContent CreateHttpContent(object content)
{
// ...
}
}
For our first call await this.mediator.Publish(new MagicAddInitIntegrationEvent(data));
the event is published and the handler can handle the event.
Looking at this.mediator
we see that there is a resolved service for it.
When we run the var response = await this.httpClient.SendAsync(request).ConfigureAwait(false);
it seems like the httpClient just decides to dispose all services for everyone. 🤔
When looking at this.mediator
after this line we see that everything is cleaned.
Since there are no services the call await this.mediator.Publish(new MagicAddIntegrationEvent(data));
wont be able to run and we get an exception.
Is that the intended behavior for this.httpClient.SendAsync
? Should we do it in another way?
One solution that seems to work is to ignoring setting the services.AddHttpClient<IMagicService, HttpMagicService>();
and instead set a singleton service for HttpClient
, this seems to work, but i´m not sure if that´s the right approach.
Solution
Used Panagiotis Kanavos suggestion and did a scoped httpClient, so my solution changed to this.
public void ConfigureServices(IServiceCollection services)
{
// other services configured above
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestLoggingBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestPerformanceBehaviour<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestValidationBehavior<,>));
services.AddMediatR(typeof(ApplicaitonCommand).GetTypeInfo().Assembly);
services.AddScoped<HttpClient>();
}
If trying to use the IMediator inside the scope without using var mediator= scope.ServiceProvider.GetRequiredService<IMediator>();
, same problem as before did arise. So there was some scope problem with the DI container that i've missed.
public class HttpMagicService : IMagicService
{
private readonly IMediator mediator;
private readonly IServiceProvider services;
public HttpMagicService(IServiceProvider services)
{
this.services = services?? throw new ArgumentNullException(nameof(services));
}
public async Task AddAsync(Magic data, Metadata metadata)
{
using (var scope = Services.CreateScope())
{
var httpClient = scope.ServiceProvider.GetRequiredService<HttpClient>();
var mediator= scope.ServiceProvider.GetRequiredService<IMediator>();
...
}
Exceptions like this often occur when a singleton service tries to consume a scoped service. This question about MediatR and scopes may be relevant. In any case, HttpClientFactory recycles HttpClient instances (specifically handler instances) periodically, which means that if HttpMagicService
caches the HttpClient instance long enough, it will get disposed at some point.
In either case, you may be able to use HttpClient by injecting IServiceProvider into HttpMagicService and creating a scope explicitly. Check consuming a scoped service in a background task.
You can inject IServiceProvider instead of HttpClient, and create a scope inside the AddAsync
method, eg :
public class HttpMagicService : IMagicService
{
private readonly IMediator mediator;
private readonly IServiceProvider services;
public HttpMagicService(IMediator mediator, IServiceProvider services)
{
this.mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
this.services = services?? throw new ArgumentNullException(nameof(services));
}
public async Task AddAsync(Magic data, Metadata metadata)
{
using (var scope = Services.CreateScope())
{
var httpClient = scope.ServiceProvider.GetRequiredService<HttpClient>();
...
}