Integrating Autorest Clients with HttpClientFactory and DI
Introductionlink
The new HttpClientFactory
feature coming in ASP.NET Core 2.1 is a great addition to the ASP.NET Core stack which helps to prevent common issues and confusion of how to work with HttpClient
.
Primarily, it will handling the life time of HttpClient
and more importantly, the underlying HttpClientHandler
properly for you.
To get more details and to find out more about the motivation and goals of HttpClientFactory
, go ahead and read Steve Gordon's blog posts.
In short, handling the life time of HttpClientHandler
isn't trivial, you cannot create too many instances (e.g. an instance per request) and you cannot use a singleton instance either!
The factory re-uses the HttpClientHandler
s to prevent client socket exhaustion issues and replaces the handlers after a while to prevent stale DNS/connections.
In addition to that, the HttpClientFactory
also integrates with Polly to provide an easy to use fluent API to configure retries, circuit breakers and all the good stuff per named client.
Autorest is a great tool from Microsoft to generate clients from swagger/Open API definitions.
Unfortunately, those two things do not work together seamlessly, yet.
My Use Caselink
I'm currently using autorest heavily at work. We have many services and apps which make many service calls to different endpoints all the time. Basically, a microservice oriented architecture, all on .NET Core.
For reasons (flexibility, reducing deploy/build dependencies), we decided to not publish NuGets with service clients. Instead, every part/app which needs to call a certain API has to generate all clients they might need. That means, there are many clients generated for the same service endpoints in many different projects.
That's works great (or isn't a problem) as long as the generated clients can be used as is. If we'd have to extend the generated clients code, partial classes and such, it gets very hard to maintain pretty quickly...
We also use the build in DI in ASP.NET Core across the board and figured that autorest clients are not really great to be injected as is. It gets even more complicated if security and also HttpClientFactory
has to be added.
The following is pseudo code to illustrate what I have today:
services.AddScoped<IPetStoreClient, PetStoreClient>(p =>
{
var tokenProvider = p.GetRequiredService<UserAccessTokenProvider>();
var serviceDiscovery = p.GetRequiredService<IServiceDiscovery>();
var httpClientFactory = p.GetRequiredService<IHttpClientFactory>();
HttpClientHandler rootHandler = httpClientFactory.CreateHandler();
ServiceClientCredentials credentials = new TokenCredentials(tokenProvider);
Uri baseUri = serviceDiscovery.GetServiceBaseUri("serviceName");
return new PetStoreClient(baseUri, credentials, rootHandler);
});
- I use client-side service discovery to find services, the IServiceDiscovery service helps to retrieve the base Uri.
- My custom implementation of
HttpClientFactory
exposes a factory method to get or create aHttpClientHandler
. This method unfotunately doesn't exist in the offical library (yet) - For security, I have to pass access tokens along. That can either be a token for a user-initiated flow or client-credentials flow, depending on the use-case...
- The generate
PetStoreClient
can be used as is, that's one of the generated constructors.
Problems with Autorest and HttpClientFactorylink
The main problem with autorest is that it doesn't work well with DI and/or with the configuration or options framework.
You cannot just use HttpClientFactory
with generated clients because there are no public constructors in the generated client which takes an instance of HttpClient
or IHttpClientFactory
(only protected).
Solutions / Discussionlink
A) Add HttpClientFactory
support to autorestlink
I think it would be great if autorest would add support for HttpClientFactory
and have a constructor with IHttpClientFactory
injected. Later, the client would call factory.CreateClient(namof(<ClientName>))
to get a named instance.
Problems here are version compatibility issues with the base runtime library and additional dependencies. The runtime would at least need a dependency to Microsoft.Extensions.Http
which comes with even more dependencies to DI etc...
So that's probably not going to happen.
Alternatively, they could just generate constructors which take HttpClient
instead of root and additional handlers. Not sure why that's not a thing...
B) Get HttpClientHandler
from IHttpClientFactory
link
The main feature (apart from Polly) of the HttpClientFactory
is handling the lifetime of those "expensive" HttpClientHandler
s.
Why not have a CreateHandler(string name)
method in addition to the CreateClient(string name)
?
In the end, DefaultHttpClientFactory
just gets or creates a handler and creates a new HttpClient
instance when calling CreateClient
(see line 117)
var entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
var client = new HttpClient(entry.Handler, disposeHandler: false);
CreateHandle
would just return entry.Handler
instead.
C) Custom Client Extensionlink
Not really an extension but the generated client is a partial class, which allows us to access the protected constructors of the base class (which gives access to the HttpClient
setter).
That allows us to use the HttpClientFactory
to create an instance and then pass it through.
Examplelink
Let’s assume we generated a client for the famous public example API of pet store:
autorest --csharp --clear-output-folder --input-file=http://petstore.swagger.io/v2/swagger.json --override-client-name=PetStoreClient --add-credentials
I can create a partial PetStoreClient
class with a new constructor which takes an HttpClient
:
public partial class PetStoreClient
{
// disposeHttpClient can be set to true, HttpClientFactory sets disposeHandler to false so that the HttpClient does not dispose the important HttpClientHandle...
public PetStoreClient(Uri baseUri, HttpClient httpClient, ServiceClientCredentials credentials)
: base(httpClient, disposeHttpClient: true)
{
BaseUri = baseUri ?? throw new ArgumentNullException(nameof(baseUri));
Credentials = credentials ?? throw new ArgumentNullException(nameof(credentials));
}
}
To inject this client, I have to create a little bit more complex factory to:
- resolve the base Uri from service discovery (or get it from configuration if you want to hardcode it)
- resolve the named
HttpClient
instance - create credentials
// injecting IHttpClientFactory and a named HttpClient
services.AddHttpClient<IPetStoreClient, PetStoreClient>()
.AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(4, (t) => TimeSpan.FromSeconds(t)));
services.AddSingleton<IServiceDiscovery, ServiceDiscovery>();
// add token providers so it can get dependencies from DI, too
// those are custom implementations of ITokenProvider
services.AddScoped<UserAccessTokenProvider>();
services.AddScoped<ClientCredentialsTokenProvider>();
services.AddScoped<IPetStoreClient, PetStoreClient>(p =>
{
// create a named/configured HttpClient
var httpClient = p.GetRequiredService<IHttpClientFactory>()
.CreateClient(nameof(IPetStoreClient));
// in this case user-initiated flow is used
var tokenProvider = p.GetRequiredService<UserAccessTokenProvider>();
// get the base Uri from service disco (service name could come from configuration again...)
// or read the Uri from configuration if you want to hard code it...
var baseUri = p.GetRequiredService<IServiceDiscovery>().GetServiceBaseUri("petStore");
return new PetStoreClient(baseUri, httpClient, new TokenCredentials(tokenProvider));
});
Conclusionslink
The best solution, in my opinion, would be to expose HttpClientHandler
in the HttpClientFactory library. That's of course up to Microsoft and might take a while until it gets released... we'll see.
Until then, or if it doesn't happen, I would use option C) and create custom constructors for my generated clients. That's a little bit additional work, but it will work even if I re-generate the clients and it is just 2 lines of code... Not too bad ;)
An example repository with some working and some pseudo code can be found on my GitHub account.
To read more about service discovery using Consul in ASP.NET Core, have a look here