Using Multiple HttpClients in .NET Core Console Application – Best Practices

api-designchttpnetrest

I'm building a .NET Core class library wrapper for a REST API that, ideally, could be used in both console applications and ASP.NET Core web applications. So far, I've based development on supporting dependency injection for the latter by creating a typed client for each group of REST API methods, i.e. one client for each group of (Index, Create, Destroy, etc.). It may be important to note that in this case, each group has same base address, and in most cases, the same authorization header would be used.

The typed clients inherit from a base client that handles some of the configuration:

public abstract class BaseClient
{
    protected readonly HttpClient _client;

    public BaseClient(IConfigService config, HttpClient client)
    {
        _client = client;
        _client.BaseAddress = new Uri(config.BaseAddress);

       // More configuration
    }
}

So I end up with something like this:

public class ClientA : BaseClient, IClientA
{
    public ClientA(IConfigService config, HttpClient client) : base(config, client) { }

    // Some methods
}

public class ClientB : BaseClient, IClientB
{
    public ClientB(IConfigService config, HttpClient client) : base(config, client) { }

    // More methods
}

And so on. With this I've written an IServiceCollection extension that registers all of these services:

public static class WrapperServiceCollectionExtensions
{
    public static IServiceCollection AddWrapper(this IServiceCollection services, string username, string key)
    {
        services.AddSingleton<IConfigService>(new ConfigService(username, key));
        services.AddHttpClient<IClientA, ClientA>();
        services.AddHttpClient<IClientB, ClientB>();

        // More clients

        return services;
    }
}

As I understand it, using this DI pattern, a new HttpClient is instantiated for each typed client, so I see no issue with a typed client being individually responsible for its own configuration. But let's say I want to create these clients in a console application. Finally, I get to my question: knowing that the clients all have the same base address and all will most likely be using the same credentials for authorization, what would be the recommended way of instantiating them in a console application? I'm inclined to create a new HttpClient for each typed client:

class Program
{
    private static HttpClient _clientA = new HttpClient();
    private static HttpClient _clientB = new HttpClient();

    static void Main(string[] args)
    {
        var config = new ConfigService("user", "key");
        var a = new ClientA(config, _clientA);
        var b = new ClientB(config, _clientB);
    }
}

But is this really necessary if the configurations would be the same for both anyways? Should I only worry about one HttpClient unless I'm dealing with multiple configurations? Should I even have multiple typed clients if the configuration would be the same for each? To me, it feels a little hacky to let each typed client successively overwrite a single HttpClient's configuration in its constructor, but in terms of behavior, unless different credentials were used, I don't think it would matter. Seriously stuck here.

Best Answer

First of all this a good question. :)

Let me try to help you by comparing the two approaches that you have mentioned.

One typed client for each group of API

Pros

  • This is a more flexible approach
    • You can apply different credentials later if it is needed
    • You can define different timeouts against different domains
    • You can use different resilient strategies (for example for idempotent operations you can introduce retry logic)
  • If a single instance fails then it does not affect the others
    • Separate monitoring can be introduced (and use circuit breakers to avoid flooding downstream systems)
  • Consumer can decide which one to use
    • Consumer can throttle the outgoing requests on a domain basis (by using for example the bulkhead)

Cons

  • If you have 10+ nearly identical groups of API then managing all clients can be challenging if it needs to be done manually
  • Configuring all clients with almost the same settings can be a tedious and error-prone job

A single shared client for all groups

Pros

  • Simplified configuration
  • Simpler credential management
  • Code might be easier to understand (it can improve readability)

Cons

  • Mixing http and https calls can cause overhead due to extensive handshaking
    • You might end up with IOException if you use wrong TLS version
    • You might end up with a SocketException if you exhaust the connection pool (too frequent connection open requests)
  • Changing the BaseAddress of the client can cause InvalidOperationException if there are one or more pending requests.
    • Same applies for other global settings like Timeout or MaxResponseContentBufferSize
  • CancelPendingRequests method call can take a while

My point is that each approach has its own strengths and trade-offs. It is up to you to decide which one to use based on the functional and non-functional requirements.