Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to inject IHttpClientFactory to a strongly typed client?

As the heading tells.

Let's say I register a strongly typed client like

var services = new ServiceCollection();

//A named client is another option that could be tried since MSDN documentation shows that being used when IHttpClientFactory is injected.
//However, it appears it gives the same exception.
//services.AddHttpClient("test", httpClient => 
services.AddHttpClient<TestClient>(httpClient =>
{
    httpClient.BaseAddress = new Uri("");                    
});
.AddHttpMessageHandler(_ => new TestMessageHandler());

//Registering IHttpClientFactory isn't needed, hence commented.
//services.AddSingleton(sp => sp.GetRequiredService<IHttpClientFactory>());
var servicesProvider = services.BuildServiceProvider(validateScopes: true);

public class TestClient
{
    private IHttpClientFactory ClientFactory { get; }
    public TestClient(IHttpClientFactory clientFactory)
    {
        ClientFactory = clientFactory;
    }

    public async Task<HttpResponseMessage> CallAsync(CancellationToken cancellation = default)
    {
        //using(var client = ClientFactory.CreateClient("test")) 
        using(var client = ClientFactory.CreateClient())
        {
            return await client.GetAsync("/", cancellation);
        }
    }
}

// This throws with "Message: System.InvalidOperationException : A suitable constructor
// for type 'Test.TestClient' could not be located. Ensure the type is concrete and services
// are registered for all parameters of a public constructor.

var client = servicesProvider.GetService<TestClient>();

But as noted in the comments, an exception will be thrown. Do I miss something bovious or is this sort of an arrangement not possible?

<edit: If IHttpClientFactory is registered, a NullReferenceException is thrown while trying to resolve client. Strange, strange.

<edit 2: The scenario I'm thinking to avoid is described and discussed also at https://github.com/aspnet/Extensions/issues/924 and maybe the way written there is one, perhaps not as satisfactory, way of avoiding some problems.

This happens in a XUnit project, probably doesn't have anything to do with the problem, but who knows. :)

<edit 3: A console program to show the problem.

using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace TypedClientTest
{

public class TestClient
{
    private IHttpClientFactory ClientFactory { get; }

    public TestClient(IHttpClientFactory clientFactory)
    {
        ClientFactory = clientFactory;
    }

    public async Task<HttpResponseMessage> TestAsync(CancellationToken cancellation = default)
    {
        using (var client = ClientFactory.CreateClient())
        {
            return await client.GetAsync("/", cancellation);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var services = new ServiceCollection();

        services
            .AddHttpClient<TestClient>(httpClient => httpClient.BaseAddress = new Uri("https://www.github.com/"));

        var servicesProvider = services.BuildServiceProvider(validateScopes: true);

        //This throws that there is not a suitable constructor. Should it?
        var client = servicesProvider.GetService<TestClient>();
    }
}
}

with install-package Microsoft.Extensions.Http and install-package Microsoft.Extensions.DependencyInjection.

Also in the exception stack there reads

at System.Threading.LazyInitializer.EnsureInitializedCore[T](T& target, Boolean& initialized, Object& syncLock, Func1 valueFactory) at Microsoft.Extensions.Http.DefaultTypedHttpClientFactory1.Cache.get_Activator() at ** Microsoft.Extensions.Http.DefaultTypedHttpClientFactory1.CreateClient(HttpClient httpClient) ** at System.Threading.LazyInitializer.EnsureInitializedCore[T](T& target, Boolean& initialized, Object& syncLock, Func1 valueFactory) at Microsoft.Extensions.Http.DefaultTypedHttpClientFactory1.Cache.get_Activator() at Microsoft.Extensions.Http.DefaultTypedHttpClientFactory1.CreateClient(HttpClient httpClient) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitTransient(TransientCallSite transientCallSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider) at TypedClientTest.Program.Main(String[] args) in C:\projektit\testit\TypedClientTest\TypedClientTest\Program.cs:line 40

which of course points towards the problem. But needs probably further debugging.

<edit 4: So going to the source, the problem is visible at https://github.com/aspnet/Extensions/blob/557995ec322f1175d6d8a72a41713eec2d194871/src/HttpClientFactory/Http/src/DefaultTypedHttpClientFactory.cs#L47 and in https://github.com/aspnet/Extensions/blob/11cf90103841c35cbefe9afb8e5bf9fee696dd17/src/HttpClientFactory/Http/src/DependencyInjection/HttpClientFactoryServiceCollectionExtensions.cs in general.

There are probably some ways to go about this now. :)

<edit 5:

So it appears calling .AddHttpClient for a typed client that has IHttpClientFactory one ends up in a "weird place". And indeed, it's not possible to use IHttpClientFactory to create a typed client of its own type.

One way of making this with named client could be something like

public static class CustomServicesCollectionExtensions
{
    public static IHttpClientBuilder AddTypedHttpClient<TClient>(this IServiceCollection serviceCollection, Action<HttpClient> configureClient)  where TClient: class
    {
        //return serviceCollection.Add(new ServiceDescriptor(typeof(TClient).Name, f => new ...,*/ ServiceLifetime.Singleton));
        servicesCollection.AddTransient<TClient>();
        return serviceCollection.AddHttpClient(typeof(TType).Name, configureClient);
    }
}

public static class HttpClientFactoryExtensions
{
    public static HttpClient CreateClient<TClient>(this IHttpClientFactory clientFactory)
    {
        return clientFactory.CreateClient(typeof(TClient).Name);
    }
}


public class TestClient
{
    private IHttpClientFactory ClientFactory { get; }

    public TestClient(IHttpClientFactory clientFactory)
    {
        ClientFactory = clientFactory;
    }

    public async Task<HttpResponseMessage> Test(CancellationToken cancellation = default)
    {
        using(var client = ClientFactory.CreateClient<TestClient>())
        {
            return await client.GetAsync("/", cancellation);
        }
    }
}

Which mimicks what the extension methods already do. It's of course now possible to expose the lifetime services better too.

like image 753
Veksi Avatar asked Jun 24 '19 15:06

Veksi


1 Answers

Please read about Typed clients:

A typed client accepts a HttpClient parameter in its constructor

Instead of IHttpClientFactory your class should accept an HttpClient in its constructor, which will be provided by DI (enabled with the AddHttpClient extension).

public class TestClient
{
    private HttpClient Client { get; }
    public TestClient(HttpClient client)
    {
        Client = client;
    }

    public Task<HttpResponseMessage> CallAsync(CancellationToken cancellation = default)
    {
        return client.GetAsync("/", cancellation);
    }
}

Edit

(based on above edits)

If you want to override the default behavior of the AddHttpClient extension method, then you should register your implementation directly:

var services = new ServiceCollection();
services.AddHttpClient("test", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://localhost");                    
});

services.AddScoped<TestClient>();

var servicesProvider = services.BuildServiceProvider(validateScopes: true);
using (var scope = servicesProvider.CreateScope())
{
    var client = scope.ServiceProvider.GetRequiredService<TestClient>();
}
public class TestClient
{
    private IHttpClientFactory ClientFactory { get; }

    public TestClient(IHttpClientFactory clientFactory)
    {
        ClientFactory = clientFactory;
    }

    public Task<HttpResponseMessage> CallAsync(CancellationToken cancellation = default)
    {
        using (var client = ClientFactory.CreateClient("test"))
        {
            return client.GetAsync("/", cancellation);
        }
    }
}
like image 149
desmondgc Avatar answered Nov 15 '22 11:11

desmondgc