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):

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();
    }
}