Search code examples
c#asp.netasp.net-coredependency-injectiondotnet-httpclient

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..


Solution

  • 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.