Integrating Autorest Clients with HttpClientFactory and DI

Introduction

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 HttpClientHandlers 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 Case

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 a HttpClientHandler. 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 HttpClientFactory

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 / Discussion

A) Add HttpClientFactory support to autorest

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

The main feature (apart from Polly) of the HttpClientFactory is handling the lifetime of those "expensive" HttpClientHandlers.

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 Extension

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.

Example

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:

  1. resolve the base Uri from service discovery (or get it from configuration if you want to hardcode it)
  2. resolve the named HttpClient instance
  3. 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));
});

Conclusions

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