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, Func
1 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.