I have a doubt related to the ASP.NET core dependency injection container. This question refers specifically to ASP.NET core 3.1.
I'm basically asking myself whether injecting a typed HTTP client inside an hosted service creates a so called captive dependency in terms of dependency injection.
The scenario I'm trying to depict is the following:
public interface IStudentsApiClient
{
Task<IEnumerable<Student>> GetAll(CancellationToken cancellationToken);
}
public class StudentsApiClient: IStudentsApiClient
{
private readonly HttpClient _httpClient;
public StudentsApiClient(HttpClient httpClient)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public async Task<IEnumerable<Student>> GetAll(CancellationToken cancellationToken)
{
// call a GET endpoint to retrieve all the students from a third party api...
}
}
public class StudentsPollingHostedService: BackgroundService
{
private readonly IStudentsApiClient _apiClient;
public StudentsPollingHostedService(IStudentsApiClient apiClient)
{
_apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// do something using the IStudentsApiClient service
}
}
// code from Startup.cs
services.AddHttpClient<IStudentsApiClient, StudentsApiClient>(); // IStudentsApiClient is registered as transient
services.AddHostedService<StudentsPollingHostedService>(); // the hosted service is registered as a singleton
In the above code I'm injecting a transient dependency (IStudentsApiClient
), inside a singleton (StudentsPollingHostedService
), thereby creating a captive dependency.
Based on my understanding, this could totally harm the whole purpose of the ASP.NET core HTTP client factory implementation, which is aimed to return a new HTTP client each time one is requested by the consumer. In the code above we basically have an instance of HTTP client captured inside the hosted service for the whole application lifetime.
Do you agree that this is an actual issue ?
My idea to fix the above code is changing the StudentsApiClient
so that the IHttpClientFactory
is used to create a new instance of HttpClient
each time GetAll
is called:
public interface IStudentsApiClient
{
Task<IEnumerable<Student>> GetAll(CancellationToken cancellationToken);
}
public class StudentsApiClient: IStudentsApiClient
{
private readonly IHttpClientFactory _factory;
public StudentsApiClient(IHttpClientFactory factory)
{
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
public async Task<IEnumerable<Student>> GetAll(CancellationToken cancellationToken)
{
var httpClient = _factory.CreateClient("students");
// call a GET endpoint to retrieve all the students from a third party api...
}
}
public class StudentsPollingHostedService: BackgroundService
{
private readonly IStudentsApiClient _apiClient;
public StudentsPollingHostedService(IStudentsApiClient apiClient)
{
_apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// do something using the IStudentsApiClient service
}
}
// code from Startup.cs
services.AddHostedService<StudentsPollingHostedService>(); // the hosted service is registered as a singleton
services.AddHttpClient("students", c =>
{
// students HTTP client configuration goes here...
});
services.AddSingleton<IStudentsApiClient, StudentsApiClient>(); // IStudentsApiClient is now registered as a singleton
Does this solve the issue of the first version of the code (if any) ?
IMPORTANT UPDATE
For all the general readers interested in this topic, there is basically the same discussion on the dotnet/runtime github repository.
It seems that the problem discussed here is an actual problem.
Take a look here for the details.
My current decision is avoiding the typed client approach and inject the IHttpClientFactory
instead, in order to be totally safe.