-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
The current implementation of the HttpClientBuilderExtensions.AddTypedClient<TClient> extension method of the Microsoft.Extensions.Http project registers the TClient with the Transient lifestyle. I think this is incorrect and would like drop this here for discussion.
With the introduction of ASP.NET Core v2.1, the new IHttpClientFactory interface and its related extension methods were added to address the problems with reuse of HttpClient instances, as discussed here, here, and here, as referenced by Steve Gordon in his blog post on this matter.
Problem being that HttpClients that are stored for the duration of the app domain can cause several issues, most obvious one being that a Singleton HttpClient doesn't respect DNS changes. This, of course, is exactly why the new extension infrastructure is put in place.
What strikes me, though, is that, although there is seemingly the observation that caching of HttpClient for the duration of the application is bad, AddTypedClient registers the HttpClient consumers as Transient, as can be seen in the AddTypedClient implementation:
public static IHttpClientBuilder AddTypedClient<TClient>(this IHttpClientBuilder builder)
where TClient : class
{
...
builder.Services.AddTransient<TClient>(s =>
{
var httpClientFactory = s.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient(builder.Name);
var typedClientFactory = s.GetRequiredService<ITypedHttpClientFactory<TClient>>();
return typedClientFactory.CreateClient(httpClient);
});
return builder;
}The HttpClientFactory sample project shows a usage of this method:
services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
})
.AddHttpMessageHandler(() => new RetryHandler())
.AddTypedClient<GitHubClient>();//GitHubClient depends on HttpClient and gets the "github" client
services.AddSingleton<IMyService, MyService>(); // Depends on GitHubClientI think the registration of Transient consumers is problematic, as Transient—in term of Microsoft.Extensions.DependencyInjection—means stateless; not short lived. This means that the container allows those stateless consumers of HttpClient to be injected into Singleton consumers without the container's Scope Verfication feature to get tripped. For instance, in the case of the Singleton MyService registration shown above, it will keep the GitHubClient alive for the duration of the application, and GitHubClient will keep its HttpClient alive for the duration of the application. This, as stated above, could lead to missing DNS changes, effectively causing the application to become corrupted in a way that can only be fixed with a restart of the application.
The injection of HttpClient instances into Singleton consumers could, therefore, lead to the type of bugs that you so hard tried to prevent using this new design. Because of the way AddHttpClient is designed, no warning signs will be given. Because of the way DNS caching works, problems only appear after the application is deployed to production; hardly ever on the developer's machine, making it crucial to warn developers in an early stage about this issue.
I think the solution to this problem is, therefore, rather straightforward (although, admittedly, a breaking change). Change the AddTypedClient method to change its TClient as Scoped instead, because in that case, MS.DI's Scope Validation feature will prevent the client to be resolved from a root scope:
public static IHttpClientBuilder AddTypedClient<TClient>(this IHttpClientBuilder builder)
where TClient : class
{
// Register client as Scoped, allowing Captive Dependencies being detected in case the
// ServiceProvider is built using the validateScopes flag.
builder.Services.AddScoped<TClient>(s =>
{
// same code as before
});
return builder;
}Although changing the lifestyle from Transient to Scoped can be considered to be a breaking change, due to how Scope Validation is configured in ASP.NET Core applications, exceptions will only be thrown in development mode; not when the application is deployed.