Skip to content

[API Proposal]: Make AddHttpClient() inject an ITypedHttpClientFactory into instantiated class #64034

Open
@Eli-Black-Work

Description

@Eli-Black-Work

Background and motivation

We currently have some code like this:

UserService.cs

public class UserService : IUserService
{
    private readonly HttpClient _httpClient;
    
    public UserService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    
    public async Task Example()
    {
        await _httpClient.GetAsync("...");
    }
}

UserRepo.cs

public class UserRepo : IUserRepo
{
    private readonly IUserService _userService;

    /// <summary>
    /// Class that allows for managing target lists including CRUD and commands
    /// </summary>
    public Operator(IUserService userService)
    {
        _userService = userService;
    }
}

Startup.cs

services
    .AddHttpClient<IUserService, UserService>(httpClient =>
    {
        httpClient.BaseAddress = new Uri(Configuration["...."], UriKind.Absolute);
    })
    .ConfigurePrimaryHttpMessageHandler(sp =>
    {
        return new HttpClientHandler() {
            UseProxy = true,
            DefaultProxyCredentials = CredentialCache.DefaultCredentials
        };
    });

services.AddSingleton<IUserRepo, UserRepo>();

This is a problem, because while IUserService is transient, IUserRepo is a singleton, so the HttpClient that's passed to IUserService will never be refreshed.

One fix is to make IUserRepo also be transient, but this isn't always ideal.

API Proposal

I propose that calling AddHttpClient() should no only inject HttpClient but should also inject a new interface, ITypedHttpClientFactory. The user could then use ITypedHttpClientFactory to create new instances of HttpClient that have the settings that were configured via AddHttpClient().

The advantage is that service singleton services that wrap typed clients could remain singletons instead of needing to be reconfigured to be transient.

API Usage

UserService.cs

public class UserService : IUserService
{
    private readonly ITypedHttpClientFactory _httpClientFactory;
    
    public UserService(ITypedHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
    
    public async Task Example()
    {
        // This HttpClient will have the BaseUrl, Proxy, etc. that was configured via the AddHttpService() call.
        using var httpClient = _httpClientFactory.CreateClient();
        
        await httpClient.GetAsync("...");
    }
}

UserRepo.cs (Same as above – no changes)

public class UserRepo : IUserRepo
{
    private readonly IUserService _userService;

    /// <summary>
    /// Class that allows for managing target lists including CRUD and commands
    /// </summary>
    public Operator(IUserService userService)
    {
        _userService = userService;
    }
}

Startup.cs (Same as above – no changes)

services
    .AddHttpClient<IUserService, UserService>(httpClient =>
    {
        httpClient.BaseAddress = new Uri(Configuration["...."], UriKind.Absolute);
    })
    .ConfigurePrimaryHttpMessageHandler(sp =>
    {
        return new HttpClientHandler() {
            UseProxy = true,
            DefaultProxyCredentials = CredentialCache.DefaultCredentials
        };
    });

services.AddSingleton<IUserRepo, UserRepo>();

Alternative Designs

No response

Risks

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions