Blazorwasm: Using AddHttpClient with Singleton does not do what you think
Intro
.NET Core has three distinct ways it injects dependencies: scoped, transient and singleton. In this post I will concentrate on singleton with what is either a bug or a quirk of the framework that can cause problems if one isn't careful.
The problem
Blazor contains an extension method that can be run during the application startup that automatically injects HttpClient to whichever service is given as a type:
Program.cs:
builder.Services.AddHttpClient<IService, Service>(...)
In this example code, Service instance at the point of injection provides HttpClient automatically. When doing this with Singleton type service however, we run into problems. It seems that when we use AddHttpClient method it overwrites any singleton services with either a transient or scoped service.
blazor-singleton-add-httpclient-demo project provides demonstration of this behaviour.
A solution
One solution to this behaviour is to use a factory service to provide the client and inject it through more traditional methods. You will need a total of three new files (potentially only two):
- IHttpFactory provides the interface
- HttpFactory is a singleton service that provides the client
- HttpOptions is an options file that is used to provide details like base url
Here's the code for all three:
IHttpFactory
public interface IhttpFactory
{
HttpClient GetClient();
}
HttpFactory
public class HttpFactory : IhttpFactory
{
private HttpOptions _options;
private HttpClient _client;
public HttpFactory(IOptions<HttpOptions> options)
{
_options = options.Value;
_client = new HttpClient
{
BaseAddress = new Uri(_options.BaseUrl)
};
}
public HttpClient GetClient()
{
return _client;
}
}
HttpOptions
public class HttpOptions
{
public string BaseUrl { get; set; }
}
Then in Program.cs we attach the factory to DI.
Program.cs
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddSingleton<ICounterService, CounterService>();
builder.Services.AddSingleton<IhttpFactory, HttpFactory>();
builder.Services.Configure<HttpOptions>(options => options.BaseUrl = "http://localhost:5000");
await builder.Build().RunAsync();
}
And finally in the service (in this case CounterService) we inject the factory and retrieve HttpClient.
CounterService
public class CounterService : ICounterService
{
private HttpClient _http;
public CounterService(IhttpFactory httpFactory)
{
_http = httpFactory.GetClient();
}
}