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.
-
1Note that you can use the same dependency injection framework used by the web app in a console application; I usually use this approach to avoid code duplication.gcali– gcali06/01/2020 07:42:37Commented Jun 1, 2020 at 7:42
1 Answer 1
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)
- You might end up with
- Changing the
BaseAddress
of the client can causeInvalidOperationException
if there are one or more pending requests.- Same applies for other global settings like
Timeout
orMaxResponseContentBufferSize
- Same applies for other global settings like
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.