Configure HttpClientFactory to use data from the current request context

Tomas Aschan picture Tomas Aschan · Jul 16, 2018 · Viewed 9.9k times · Source

With the new HttpClientFactory in ASP.NET Core 2.1, it's quite easy to configure custom HTTP clients with things like base urls, default headers etc.

However, I haven't found a way to centralize configuration that lets me inject headers from the current request context. For example, consider a service called with an Authorization header, which I wish to pass on to all the underlying services as well. It would be awesome to be able to configure this in the .AddHttpClient() call on services in the Startup class, but I can't figure out how to get at the request context from there.

Any ideas?

Answer

Ruard van Elburg picture Ruard van Elburg · Jul 21, 2018

Working on this answer lead me to multiple answers. I think the first approach is what you are looking for, the second is a good alternative.

In order to configure multiple clients you can use named clients. These clients are registered as transient. Use DI to get the service that has access to the request context.

For that we need IHttpContextAccessor. In this case you don't have to register it yourself, because Identity already does that for you.

Otherwise add the following line in startup:

services.AddHttpContextAccessor();

Next we can configure the named client "github":

services.AddHttpClient("github", c =>
{
    // access the DI container
    var serviceProvider = services.BuildServiceProvider();
    // Find the HttpContextAccessor service
    var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
    // Get the bearer token from the request context (header)
    var bearerToken = httpContextAccessor.HttpContext.Request
                          .Headers["Authorization"]
                          .FirstOrDefault(h => h.StartsWith("bearer ", StringComparison.InvariantCultureIgnoreCase));

    // Add authorization if found
    if (bearerToken != null)
        c.DefaultRequestHeaders.Add("Authorization", bearerToken);

     // Other settings
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // Github API versioning
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // Github requires a user-agent
});

Call the client like this:

public class MyController : ControllerBase
{
    private readonly IHttpClientFactory _clientFactory;

    public MyController(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task<ActionResult> StartCall()
    {
        var client = _clientFactory.CreateClient("github");
        var response = await client.GetAsync("/repos/aspnet/docs/issues");
    }
}

Another option is to use Typed clients. Here's a short example. For a full example check the link.

Register IHttpContextAccessor:

services.AddHttpContextAccessor();

Create a typed client. I've added two options to add settings. One through the request context and one through a singleton class:

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client, HttpClientSettings httpClientSettings, IHttpContextAccessor httpContextAccessor)
    {
        var bearerToken = httpContextAccessor.HttpContext.Request
                              .Headers["Authorization"]
                              .FirstOrDefault(h => h.StartsWith("bearer ", StringComparison.InvariantCultureIgnoreCase));

        // Add authorization if found
        if (bearerToken != null)
            client.DefaultRequestHeaders.Add("Authorization", bearerToken);

        // Or the value from httpClientSettings:
        client.DefaultRequestHeaders.Add("Authorization", httpClientSettings.BearerToken);

        client.BaseAddress = new Uri("https://api.github.com/");
        client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // GitHub API versioning
        client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // GitHub requires a user-agent

        Client = client;
    }
}

Register the client:

// The typed client is registered as transient with DI.
services.AddHttpClient<GitHubService>();

Please note, the code below is just an example. Since the token can't be persisted in the client, you can use the shared HttpClientSettings instead:

services.AddSingleton<HttpClientSettings>();

Where HttpClientSettings is:

public class HttpClientSettings
{
    public string BearerToken { get; set; }
}

You can use the client like this:

public class MyController : ControllerBase
{
    private readonly GitHubService _gitHubService;

    public MyController(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task<ActionResult> StartCall()
    {
        var response = await _gitHubService.Client.GetAsync("/repos/aspnet/docs/issues");

    }
}