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.DefaultTypedHttpClientFactory
1.Cache.get_Activator() at ** Microsoft.Extensions.Http.DefaultTypedHttpClientFactory1.CreateClient(HttpClient httpClient) ** 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.DefaultTypedHttpClientFactory
1.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.
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);
}
}
(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);
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With