2

I thought I understood how AddHttpClient worked, but apparently, I do not. I've distilled this problem down to the very basics, and it still isn't functioning as I expect.

I have the following class and interface:

public interface ISomeService
{
  Uri BaseAddress { get; }
}

public class SomeService : ISomeService
{
  private readonly HttpClient _client;

  public SomeService(HttpClient client)
  {
      _client = client;
  }

  public Uri BaseAddress => _client.BaseAddress;
}

Simply exposing the BaseAddress for the purposes of this example.

Now, I perform the following:

[Fact]
public void TestClient()
{
    var services = new ServiceCollection();
    services.AddHttpClient<SomeService>((serviceProvider, client) =>
    {
        client.BaseAddress = new Uri("http://fakehost");
    });
    services.AddSingleton<ISomeService, SomeService>();
    var provider = services.BuildServiceProvider();
    var svc = provider.GetRequiredService<ISomeService>();
    svc.BaseAddress.Should().Be("http://fakehost");
}

and it fails, because the base address is null, instead of http://fakehost like I expect.

This is because somehow, SomeService gets an HttpClient created for it without going through my configure method (I added a breakpoint, it never got hit). But like magic I still an actual constructed instance of SomeService.

I've tried adding the HttpClient typed to the interface instead, no luck.

I found that if I GetRequiredService<SomeService>, instead of for ISomeService the code behaves as expected. But I don't want people injecting concrete SomeService. I want them to inject an ISomeService.

What am I missing? How can I inject an HttpClient already configured with a base address to a service that will be, itself, injected via DI (with other potential dependencies as well).

Background: I'm building a client library with ServiceCollectionExtensions so that consumers can just call services.AddSomeService() and it will make ISomeService available for injection.

EDIT:

I have since found (through experimentation) that .AddHttpClient<T> seems to add an HttpClient for T only when explicitly trying to resolve T, but also adds a generic HttpClient for any other class.

commenting out the AddHttpClient section of my test resulted in failed resolution but changing the to AddHttpClient<SomeOtherClass> allowed ISomeService to resolve (still with null BaseAddress though).

EDIT 2:

The example I posted originally said I was registering ISomeService as a singleton, which I was, but after inspection, I've realized I don't need it to be so switching to transient allows me to do

.AddHttpClient<ISomeService, SomeService>(...)

AceGambit
  • 423
  • 3
  • 11
  • Dogs are not cats even if they are have same name ... – Selvin Jan 20 '23 at 13:15
  • @Selvin, I realize that, but I would have thought that the DI container, when trying to resolve an instance of `ISomeService` would understand that means an instance of `SomeService` needs to be created, at which point it would see that an `HttpClient` dependency exists and a configure method for `HttpClient` in `SomeService` has been registered and use that. This doesn't seem to be the case though, and I'm not sure why, or how to do this differently so that it behaves as expected. – AceGambit Jan 20 '23 at 13:22
  • You could inject IHttpClientFactory and call something like `CreateClient(nameof(SomeService))` on it in the SomeService constructor. Your registration should still work then. – Ralf Jan 20 '23 at 13:27
  • I think you can just use `AddHttpClient` and it should work out. – Jeanot Zubler Jan 20 '23 at 13:29
  • @JeanotZubler, no luck on that front. Edited post to reflect my experience – AceGambit Jan 20 '23 at 13:35
  • `svc.BaseAddress` is Uri and `"http://fakehost"` is string ... you cannot compare them – Selvin Jan 20 '23 at 13:37
  • `I have since found (through experimentation) that .AddHttpClient seems to add an HttpClient for T only when explicitly trying to resolve T` that's in the docs, it doesn't require experimentation. You need to use `AddHttpClient(...)` if you want to register the typed client `SomeService` as an implementation of `ISomeService` – Panagiotis Kanavos Jan 20 '23 at 13:40
  • `services.AddSingleton` itself is a bug. HttpClientFactory recycles HttpClientHandlers. If the typed client `SomeService` is a singleton, at some point it will end up trying to use a recycled HttpClientHandler. `AddHttpClient` registers typed clients as transient, so every class that needs one gets a new one with a valid, possibly cached, HttpClientHandler – Panagiotis Kanavos Jan 20 '23 at 13:48

1 Answers1

1

Internally build in DI works with ServiceDescriptors which represent implementation type-service type pairs. AddHttpClient<SomeService> registers SomeService as SomeService which has nothing to do with ISomeService from DI standpoint, just provide service type and implementation type:

services.AddHttpClient<ISomeService, SomeService>((serviceProvider, client) =>
{
    client.BaseAddress = new Uri("http://fakehost");
});

Though it will register the service type as transient.

If you need to have it as singleton - you can try reabstracting (though it should be done with caution as mentioned in the comments):

services.AddHttpClient<SomeService>((serviceProvider, client) =>
{
    client.BaseAddress = new Uri("http://fakehost");
});
services.AddSingleton<ISomeService>(sp => sp.GetRequiredService<SomeService>());
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • `AddHttpClient` or `` register a typed HTTP client whose lifecycle is kind-of controlled by the HttpClientFactory. The type T itself is transient but HttpClientFactory caches and reuses the HttpClientHandlers. That's essential for socket caching and recycling. It's definitely not a general-purpose service registration call – Panagiotis Kanavos Jan 20 '23 at 13:43
  • @PanagiotisKanavos yes , I was writing about service lifetime, not `HttpClient`. – Guru Stron Jan 20 '23 at 13:45
  • Wouldn't `services.AddSingleton` risk using an already recycled HttpClientHandler? – Panagiotis Kanavos Jan 20 '23 at 13:45
  • Explanation: `services.AddHttpClient` does not really "add a preconfigured `HttpClient`to `T`", but rather "register `T` as my special version of a HttpClient, with a preconfigured underlying `HttpClient`" – Jeanot Zubler Jan 20 '23 at 13:45
  • @PanagiotisKanavos what difference does it make compared to transient lifetime? No one can guarantee that a transient service will not be used inside a singleton one. I have not dug too deep but I assume that this should not be a problem. – Guru Stron Jan 20 '23 at 13:47
  • The typed client is singleton by the HttpClientHandler is not. At some point, the HttpClientFactory may recycle it, leaving the singleton client with an invalid handler. Even if it doesn't, the HttpClientHandler can end up pointing to the wrong IP. HttpClientFactory is used to avoid exactly this situation – Panagiotis Kanavos Jan 20 '23 at 13:51
  • The HttpClient class doesn't know about the Factory and can't renew its HttpClientHandler. The HttpClientFactory isn't even in the same NuGet package – Panagiotis Kanavos Jan 20 '23 at 13:51
  • I don't actually need the service to be singleton as it doesn't have any state, so I'm gonna go with the `AddHttpClient(...)` approach since that solves my problem. I'll mark this as the selected answer, thanks! – AceGambit Jan 20 '23 at 13:55