10

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.

Enrico Massone
  • 6,464
  • 1
  • 28
  • 56
  • 1
    You should definitely read [issue #36067](https://github.com/dotnet/runtime/issues/36067) about this exact topic in the [dotnet/runtime repo](https://github.com/dotnet/runtime). It confirms your assessment. – Steven Jul 16 '20 at 17:04
  • @Steven thanks it is exactly the same question. Do you have an idea why the Microsoft DI container only raise an error for captive dependencies having a scoped lifetime ? I mean, the captive dependency anti pattern is caused from both the transient and the scoped lifetime when injected inside a singleton. – Enrico Massone Jul 17 '20 at 05:11
  • @Steven maybe, as you pointed out in your question, the transient lifetime is intended for stateless services only, so the idea is that a captive transient dependency is not an harm because there is no "captive state" which can be corrupted. Unfortunately there is a state involved in the HttpClient due to the underlying http message handler and its connection pool and cached DNS resolution. – Enrico Massone Jul 17 '20 at 05:13
  • 1
    Hi Enrico, after reading your response on issue 36067, as far as I understand the details, your understanding of the issue is spot on. I don't really understand why those two Microsoft employees respond (with with incorrect assumptions) without doing their homework first. – Steven Jul 17 '20 at 13:35
  • 1
    And your understanding of what 'transient' means in the context of MS.DI is correct as well. The `HttpMessageHandler` contains state, which means `transient` is not the proper lifestyle for `HttpClient`. Unfortunately, MS "didn't put a ton of thought into whether this should be transient or scoped," and ignored the issue for 18 months, before deciding that the change "is too risky to take for 5.0." – Steven Jul 17 '20 at 13:39
  • @Steven thanks for the confirmation. I'll follow up the github issue looking for future comments. For the moment I'll opt for injecting the IHttpClientFactory and avoiding the typed client approach, at least until a feedback from the .net core team will be available. Thanks for the contribution here. – Enrico Massone Jul 17 '20 at 15:35
  • 1
    I wrote [some guidance](https://github.com/simpleinjector/SimpleInjector/issues/654#issuecomment-454051502) on using Typed HttpClients for Simple Injector, which you may find useful. – Steven Jul 17 '20 at 15:42
  • My workaround with ms di was to not inject `HttpClient` via `services.AddHttpClient()`, instead have my own factory which serves singleton http client instances based on use case. Very much like `Flurl` per url client factory. – andrei.ciprian Jul 19 '20 at 12:17

0 Answers0