In C#, How can I reuse an HttpClient for multiple services? - c#

I've created a C# microservice which offers several different (but related) functions.
I am now creating a C# Nuget package for a client which will help other C# microservices to leverage this microservice. This will include a Service Collection Extension class to facilitate adding the client to those microservices.
In the interest of separating concerns, within the client, I've separated the functionality into three classes:
SalesforceCacheQuerier
SalesforceCacheSyncDataManipulator
SalesforceCacheAsyncDataManipulator
Each of these need to call out to the same server.
As a niave first implementation, I've composed this method:
public static IServiceCollection AddSalesforceClients(this IServiceCollection services)
{
services.AddTransient<SalesforceCacheAuthenticationHandler>();
ConfigureClient(services.AddHttpClient<ISalesforceCacheQuerier, SalesforceCacheQuerier>());
ConfigureClient(services.AddHttpClient<ISalesforceCacheSyncDataManipulator, SalesforceCacheSyncDataManipulator>());
ConfigureClient(services.AddHttpClient<ISalesforceCacheAsyncDataManipulator, SalesforceCacheAsyncDataManipulator>());
return services;
}
private static IHttpClientBuilder? ConfigureClient(IHttpClientBuilder? clientBuilder)
=> clientBuilder.ConfigureHttpClient(ConfigureClient)
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler())
.AddHttpMessageHandler<SalesforceCacheAuthenticationHandler>();
private static void ConfigureClient(IServiceProvider provider, HttpClient client)
{
SalesforceCacheSettings? settings = provider.GetRequiredService<SalesforceCacheSettings>();
client.BaseAddress = new Uri(settings.BaseUrl, settings.ApiEndpoint);
client.DefaultRequestHeaders.ExpectContinue = true;
}
However, this generates three separate HttpClients and triples the traffic for the "Identity Server" used to provide Jwt tokens.
How can I refactor this to create and reuse only a single HttpClient?

You can use HttpClientFactory and inject it in ConfigureServices
something like services.AddHttpClient();
Later wherever you need client object just give IHttpClientFactory httpClientFactory in the constructor and you can have access to client object by just asking the factory to create a client.
HttpClient = HttpClientFactory.CreateClient(); in this fashion.
In case if you want to hold the authentication for the clients and reuse it. I would suggest to maintain a dictionary for each request type.
var HttpClients = new Dictionary<string, HttpClient>();
HttpClients.Add(SalesforceCacheQuerierKey, SalesforceCacheQuerierClient);
//assuming all the auth related headers are added to this client object //SalesforceCacheQuerierClient
and pass these from startup level.

What I needed was something like this:
services.AddHttpClient(SALESFORCE_CACHE_CLIENT, (provider, client) =>
{
SalesforceCacheClientSettings? settings = provider.GetRequiredService<SalesforceCacheClientSettings>();
client.BaseAddress = new Uri(settings.BaseUrl, settings.ApiEndpoint);
client.DefaultRequestHeaders.ExpectContinue = true;
})
.AddHttpMessageHandler<SalesforceCacheAuthenticationHandler>();
services.AddHttpClient<ISalesforceCacheQuerier, SalesforceCacheQuerier>(SALESFORCE_CACHE_CLIENT);
services.AddHttpClient<ISalesforceCacheSyncDataManipulator, SalesforceCacheSyncDataManipulator>(SALESFORCE_CACHE_CLIENT);
services.AddHttpClient<ISalesforceCacheAsyncDataManipulator, SalesforceCacheAsyncDataManipulator>(SALESFORCE_CACHE_CLIENT);

Related

Injected HttpClient ignores IHttpClientFactory configuration

I've created a custom library which automatically sets up Polly policies for specific services which depend on HttpClient.
This is done using the IServiceCollection extension methods and the typed client approach. A simplified example:
public static IHttpClientBuilder SetUpFooServiceHttpClient(this IServiceCollection services)
{
return services
.AddHttpClient<FooService>()
.AddPolicyHandler(GetRetryPolicy());
}
The example service:
public class FooService
{
private readonly HttpClient _client;
// OPTION 1
public FooService(HttpClient httpClient)
{
_client = httpClient;
}
// OPTION 2
public FooService(IHttpClientFactory httpClientFactory)
{
_client = httpClientFactory.CreateClient(GetType().Name);
}
public void DoJob()
{
var test = _client.GetAsync("http://example.com");
}
}
Fetching the service from the DI container (this is from a test project):
var services = new ServiceCollection();
services.SetUpFooServiceHttpClient();
services.AddSingleton<FooService>();
var fooService = services
.BuildServiceProvider()
.GetRequiredService<FooService>();
// Perform test
fooService.DoJob();
Note: In this test project, I also add an extra mocked handler since I'm trying to mock http status responses, but the mocked handler being present or not is identical to the Polly policy being present or not, so I omitted the mocked handler from the example code.
Notice the two different constructors in FooService. Depending on which one I comment out and which one I leave in, I get different outcomes. All other code remains untouched.
Option 1, injecting the HttpClient directly, ignores all my configuration. I get a standard http client with no Polly policy handler.
Options 2, injecting the IHttpClientFactory and requesting the client using the current type name (i.e. FooService) respect my configuration. I get a custom http client which contains the Polly policy handler (and any other handlers I may have configured, e.g. mocked handlers in my test suite)
The absence/existence of the policy handler is confirmed in both cases using debug inspection.
According to all documentation I've found on the subject, both options should be equivalent, at least in regards to the constructed HttpClient that I obtain in the end. But that is not the case here.
The documentation I find specifies that HttpClient can be injected when using typed clients:
The MSDN documentation, specifically the "typed clients" section example.
This SO answer
This blog post
I'm using a typed client but injecting a HttpClient clearly doesn't work for me.
Why does injecting a HttpClient vs injecting IHttpClientFactory work differently in my case?
In effect, you have the following two registrations for your FooService class:
services.AddHttpClient<FooService>()
services.AddSingleton<FooService>();
Because of how the DI container works behind-the-scenes, the second registration overwrites the first. If you remove the second registration, the first will be used, and so your constructor with the HttpClient parameter will be invoked.

Is this a bad pattern for using HttpClient?

I have a web application in .NET Core that leverages a third-party api and I'm wondering if a 'better' pattern would be to encapsulate all of these typed http client services into a single service that then, gets injected to each of the services, rather than injecting a new client into them. From what I've read on HttpClient it seems the optimal usage is to have a single HttpClient instance used for the whole application. All of them target the same base api but are seperated by the different endpoints/features.
I have some code in my Startup class that reads something like this
var _thirdPartyAppKey = Configuration["ThirdPartyConfig:ThirdPartyAppKey"];
services.AddHttpClient<IAuthenticationService, AuthenticationService>(client =>
{
client.BaseAddress = new Uri("https://api.thirdparty.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("Thirdparty-App-Key", _thirdPartyAppKey);
});
services.AddHttpClient<ICustomerService, CustomerService>(client =>
{
client.BaseAddress = new Uri("https://api.thirdparty.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("Thirdparty-App-Key", _thirdPartyAppKey);
});
services.AddHttpClient<ITransactionService, TransactionService>(client =>
{
client.BaseAddress = new Uri("https://api.thirdparty.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("Thirdparty-App-Key", _thirdPartyAppKey);
});
services.AddHttpClient<IConsumerService, ConsumerService>(client =>
{
client.BaseAddress = new Uri("https://api.thirdparty.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("Thirdparty-App-Key", _thirdPartyAppKey);
});
I was thinking of refactoring to something like this:
services.AddHttpClient<IThirdPartyClientService, ThirdPartyClientService>(client =>
{
client.BaseAddress = new Uri("https://api.thirdparty.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("Thirdparty-App-Key", _thirdPartyAppKey);
});
services.AddScoped<IAuthenticationService, AuthenticationService>();
services.AddScoped<ICustomerService, CustomerService>();
... the rest
And then if my ThirdPartyClientService class was just this:
public class ThirdPartyClientService {
public HttpClient _httpClient;
public ThirdPartyClientService(HttpClient httpClient) {
_httpClient = httpClient;
}
}
I could inject it into my other services and just use it like:
_thirPartyClientService._httpClient.PostAsync() etc..
If all you're doing is publicly exposing the underlying HttpClient within the typed client class and calling its PostAsync() method directly and all the different versions share the exact same settings, then you're not really deriving any value from the typed client anyway; a lot of the value of that typed client is in offering explicit abstractions over the top of the HttpClient that is sitting underneath, such as having DoSomeTransactionServiceSpecificOperation() instead of exposing the HttpClient to the consumer. Furthermore, each typed client is creating a different named base handler in HttpClientFactory's handler pool, and if that base handler is essentially the same, a single handler could be reused across all of them instead.
That said, if you do start taking advantage of abstracted methods over the top of the HttpClient for each of the different interfaces, you could either:
1) Keep the separate clients if there's reasonable expectation that their incoming parameters and methods will be unique, thereby letting them continue to be single responsibility.
or
2) Keep the separate interfaces but still only have the one concrete implementation that covers all of them, i.e. public class ThirdPartyClient : IAuthenticationService, ITransactionService ... and register the single typed client to each of the interfaces at startup. This way, when the client is injected somewhere by one interface, it will only be scoped to the methods of that interface, but you can continue managing the shared code in a single implementation until it no longer makes sense to do so, and as a bonus, the underlying handlers will be shared in the pool.

Long lived HttpClient created by HttpClientFactory

I've read that HttpMessageHandlers are recycled every 2 minutes, but I'm not sure if a new one is assigned to an existing HttpClient?
I've tested it out by using SetHandlerLifetime(TimeSpan.FromSeconds(5)); and even after 2 minutes with countless requests, the httpClient is continuing to work, which is a good sign?
Does that mean that I have no cause for concern with DNS changes/socket exhaustion?
Inside ConfigureServices method:
var myOptions = app.ApplicationServices.GetService<IOptionsMonitor<MyOptions>>();
HttpClient httpClient = app.ApplicationServices.GetService<IHttpClientFactory>().CreateClient();
MyStaticObject.Configure(myOptions, httpClient);
EDIT: Added some sample code.
There are a few things to look at here, with the first being does MyStaticObject actually need to be static? If it does, I would recommend instead registering it as a Singleton so that you can still leverage dependency injection. Once you have done that, you can register IHttpClientFactory and use it from your code. Your ConfigureServices method may end up looking something like this
public void ConfigureServices(IServiceCollection services)
{
//The base extension method registers IHttpClientFactory
services.AddHttpClient();
services.AddSingleton<IMySingletonObject, MySingletonObject>();
}
Then in your consuming class, MySingletonObject in this case, you would configure it as such
public class MySingletonObject
{
private readonly IHttpClientFactory _clientFactory;
public MySingletonObject(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task SomeMethodThatUsesTheClient()
{
var client = _clientFactory.CreateClient();
//use the client
}
}
The reason for this is that IHttpClientFactory handles the lifetime and pool concerns for us. Per the docs:
Manages the pooling and lifetime of underlying HttpClientMessageHandler instances. Automatic management avoids common DNS (Domain Name System) problems that occur when manually managing HttpClient lifetimes.
This happens when you make the CreateClient call, so you want to do this inside the code using the client, as opposed to on startup of your application.
As a side note, if you do not need this class to be a singleton at all, you can use the extenion services.AddHttpClient<IMyClass, MyClass>() and inject an HttpClient directly into the class. The DI container will handle getting a client from the factory behind the scenes for you.
public static IServiceCollection AddApiClient(this IServiceCollection services,
Action<RgCommunicationClientOptions> action)
{
services.AddHttpClient<ISomeApiClient, SomeApiClient>()
.AddPolicyHandler(GetRetryPolicy())
.SetHandlerLifetime(TimeSpan.FromMinutes(4));
services.AddOptions();
services.Configure(action);
return services;
}
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
//.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(2, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
services.AddApiClient(options => options.BaseAddress = configuration.GetSection("ExternalServices:SomeApi")["Url"]);
You can inject HttpClient like above in the class via dependency injection. I have used Poly extension to add RetryPolicy. Also you can set the LifetTime of the handler by using SetHaandlerLifeTime. This way you can individually configure client for each client. In the logs you can see that httpHandler expires after 4 mins for the respective call. I have used the extension method to pass the options in the method, getting values via app settings.
Hope this helps.
Blockquote

Dependency injection: HttpClient or HttpClientFactory?

Everywhere I can see three main approaches to create clients (basic, named, typed) in DI, but I have found nowhere if to inject IHttpClientFactory or HttpClient (both possible).
Q1: What is the difference between injecting IHttpClientFactory or HttpClient please?
Q2: And if IHttpClientFactory is injected, should I use factory.CreateClient() for each call?
Summary
HttpClient can only be injected inside Typed clients
for other usages, you need IHttpClientFactory
In both scenarios, the lifetime of HttpClientMessageHandler is managed by the framework, so you are not worried about (incorrectly) disposing the HttpClients.
Examples
In order to directly inject HttpClient, you need to register a specific Typed service that will receive the client:
services.AddHttpClient<GithubClient>(c => c.BaseAddress = new System.Uri("https://api.github.com"));
Now we can inject that inside the typed GithubClient
public class GithubClient
{
public GithubClient(HttpClient client)
{
// client.BaseAddress is "https://api.github.com"
}
}
You can't inject the HttpClient inside AnotherClient, because it is not typed to AnotherClient
public class AnotherClient
{
public AnotherClient(HttpClient client)
{
// InvalidOperationException, can't resolve HttpClient
}
}
You can, however:
1. Inject the IHttpClientFactory and call CreateClient(). This client will have BaseAddress set to null.
2. Or configure AnotherClient as a different typed client with, for example, a different BaseAdress.
Update
Based on your comment, you are registering a Named client. It is still resolved from the IHttpClientFactory.CreateClient() method, but you need to pass the 'name' of the client
Registration
services.AddHttpClient("githubClient", c => c.BaseAddress = new System.Uri("https://api.github.com"));
Usage
// note that we inject IHttpClientFactory
public HomeController(IHttpClientFactory factory)
{
this.defaultClient = factory.CreateClient(); // BaseAddress: null
this.namedClient = factory.CreateClient("githubClient"); // BaseAddress: "https://api.github.com"
}
Sadly I cannot comment, but only Post an answer. Therefore I suggest you should check out the following Links:
https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/
Regarding your Questions it more or Less boils down to this:
Q1 -> IHttpClientFactory handles the connection pools of HttpClient instances and this will help you regarding load and dispose problems as discribed in the links, if the HttpClient is used wrong.
Q2 -> yes you should use factory.create client according to microsoft docs

Prevent IHttpClientFactory to create DI scope for handlers

I am using scoped service called IOperationContextProvider to hold some information about my current execution context (called OperationContext).
Whenever I start a new execution path (not only HTTP request, but some async impulses such as queue message, change feed change..), I create a dedicated DI service scope.
Any class can inject the provider and has access to this context (such as correlation ID).
For outgoing requests, I would like to configure to add the correlation ID to outgoing HTTP header, like this:
services.AddHttpClient<IMyClass, MyClass>((serviceProvider, httpClient) =>
{
var contextProvider = serviceProvider.GetRequiredService<IOperationContextProvider>();
var corrId = contextProvider.Context.CorrelationId;
httpClient.DefaultRequestHeaders.Add("x-corr-id", corrId);
});
However, I am unable to do this, because IHttpClientFactory creates scope for each handler it is creating and my context is not reachable from inside the HTTP client configuration. Same goes for adding HTTP message handlers, they are created in the same scope as the handler too.
Official documentation:
The IHttpClientFactory creates a separate DI scope for each handler. Handlers are free to depend upon services of any scope.
Is there any way to reach the same scope as in which the HttpClient itself is being built?
I only have found a way to where for the MyClass, where I also inject HttpClient, I inject the IOperationContextProvider too and configure manually the HttpClient but that is a bit cumbersome because it needs to be done everywhere:
public MyClass(HttpClient httpClient, IOperationContextProvider contextProvider)
{
var corrId = contextProvider.Context.CorrelationId;
httpClient.DefaultRequestHeaders.Add("x-corr-id", corrId);
this._httpClient = httpClient;
}
If you absolutely don’t want the HttpClientFactory to create a service scope, then you can disable this behavior through the HttpClientFactoryOptions.SuppressHandlerScope property. There isn’t a nice API to configure this though, so you will have to do something like this:
var httpClientBuilder = services.AddHttpClient<IMyClass, MyClass>(…);
services.Configure<HttpClientFactoryOptions>(httpClientBuilder.Name, options =>
{
options.SuppressHandlerScope = true;
});
Alternatively, you could also create the delegating handler directly, without going through DI:
services.AddHttpClient<IMyClass, MyClass>(…)
.AddHttpMessageHandler(sp =>
{
var contextProvider = sp.GetService<IOperationContextProvider>()
return new MyHandlerWithoutDI(contextProvider);
});
One of the things thats also suggested is that the shared settings like the defaultrequestheaders be properly setup to avoid race conditions, if you are planning to use this client as a shared resource. This is in reference to your initial proposed workaround.
public MyClass(HttpClient httpClient, IOperationContextProvider
contextProvider)
{
var corrId = contextProvider.Context.CorrelationId;
httpClient.DefaultRequestHeaders.Add("x-corr-id", corrId);
this._httpClient = httpClient;
}
https://learn.microsoft.com/en-us/azure/architecture/antipatterns/improper-instantiation/

Categories