Search code examples
c#.net-6.0dotnet-httpclient.net-7.0

Can you add httpclients to services without having to specify each one


I have a .NET 6 app that uses Microsoft.Extensions.Http. A typical service is defined by an interface and the implementation as follows:

public interface IAddressService
{
    Task<List<Address>?> List();
    Task<List<Address>?> List(int appid);
    Task<Address?> Get(int id);
    Task<HttpResponseMessage?> Update(Address Address);
    Task<Address?> Add(Address Address);
}

public class AddressService : IAddressService
{
    private readonly HttpClient _httpClient;

    public AddressService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    async Task<List<Address>?> IAddressService.List()
    {
        List<Address>? ret;

        var response = await _httpClient.GetAsync($"api/Addresses");

        if (response.IsSuccessStatusCode)
        {
            ret = await JsonSerializer.DeserializeAsync<List<Address>>
                          (await response.Content.ReadAsStreamAsync(), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
        }
        else
        {
            ret = new List<Address>();
        }

        return ret;
    }

and so forth. I have one for each table of the database. In the ConfigureServices, I wire them up like this:

services.AddTransient<MPSHttpHandler>();

services.AddHttpClient<IAddressService, AddressService>(client =>
{
   client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"] ?? "");
}).AddHttpMessageHandler<MPSHttpHandler>();

I have to specify every service(client). I was wondering if there were a way to do something like:

var clients = typeof(Program).Assembly
            .GetTypes()
            .Where(t => t.Name.EndsWith("Service"))
            .ToList();

clients.ForEach(client =>
{
    services.AddHttpClient<client.Interface, client.Implementation>(client =>
    {
        client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"] ?? "");
    }).AddHttpMessageHandler<MPSHttpHandler>();
});                    

I don't know how to get the client type to a form where I can use them in the call to AddHttpClient. Is there a way to do this?


Update #1

I implemented the method per the example by @Guru Stron. I changed the types to match the signature of AddHttpClient. I created the static class as follows:

public static class MyHttpClientRegExts
{
    public static IServiceCollection AddCustomHttpClient<TClient, TImplementation>(this IServiceCollection services,
        IConfiguration Configuration)
    {
        services.AddHttpClient<TClient, TImplementation>(client =>
        {
            client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"] ?? "");
        }).AddHttpMessageHandler<MPSHttpHandler>();

        return services;
    }
}

I get the following error on the lambda action for client =>

CS1643: Not all code paths return a value in method of type 'func<HttpClient, TImplementaion>'

This tells me that it is using the overload

AddHttpClient<TClient,TImplementation>(IServiceCollection, Func<HttpClient,TImplementation>)

rather than the overload:

AddHttpClient<TClient,TImplementation>(IServiceCollection, Action<HttpClient>)

Is there a way to fix this?


Solution

  • You can use a bit more reflection by constructing and invoking generic method (the AddHttpClient implementation does some internal stuff so it would be better to reuse then trying to repeat). The easiest way would be to move the setup into separate static class:

    public static class MyHttpClientRegExts
    {
        public static IServiceCollection AddCustomHttpClient<TClient, TImplementation>(this IServiceCollection services,
            IConfiguration Configuration) where TClient : class where TImplementation : class, TClient
        {
            services.AddHttpClient<TClient, TImplementation>(client =>
            {
                client.BaseAddress = new Uri(...);
            }).AddHttpMessageHandler<MPSHttpHandler>();
    
            return services;
        }
    }
    

    And then use it to setup every service. Something to get you started:

    var types = typeof(Program).Assembly
        .GetTypes()
        .Where(t => t is { IsAbstract: false, IsClass: true } && t.Name.EndsWith("Service")) // find implementation types
        .ToList();
    
    // find the setup method
    var method = typeof(MyHttpClientRegExts).GetMethod(nameof(MyHttpClientRegExts.AddCustomHttpClient));
    foreach (var implType in types)
    {
        // find interface type to register, maybe will need some refinement 
        var interfaceType = implType.GetInterfaces().Single(i => i.Name.EndsWith("Service"));
        // create closed generic method and invoke to register the type pair
        method.MakeGenericMethod(interfaceType, implType)
            .Invoke(null, new object?[] { services, Configuration });
    }