.NET Core Log Correlation - HttpClient and default headers

How to pass a header to a backend API using the HttpClient easily and consistently. This header is used for correlating log entries between a frontend site, and the backend API.

In the final post of the series I am going to go over how to pass a header to a backend API using the HttpClient easily and consistently. This header is used for correlating log entries between a frontend site, and the backend API.

This is the 4th and final post of my series on log correlation between a frontend website and a back end service.

  1. Send an ID between API calls and have it included in the log files.
  2. The ID should be in the same property in all logs.
  3. Easily access the correlation ID.
  4. When possible, it should automatically be added to the correct HttpClient requests.

We'll be working in the frontend site in this post, so the namespace is going to start with Vecc.LogCorrelation.Example.Source and not Vecc.LogCorrelation.Example.Target. Everything we did for the backend API (except for the LogHeaderMiddleware class content), during the past 3 posts is also done on the frontend project, with the namespaces changed to the Target instead of Source. The only difference is in the logging middleware class. The one in the frontend site sets the correlation id in the context instead of reading the one from the header.

If you're not using the HttpClientFactory package to manage your HttpClient objects, I highly recommend that you do. There are several benefits, namely management of your connections. We will be using the HttpClientFactory in this post.

If you want to learn more about how the client factory works and how to correctly use it, take a look at the docs at https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests. If the link dies, just Google/Bing HttpClientFactory. That will give you loads of information.

One hard to find piece of information, is that the client is created in a transient scope. This means you need to architect and build your client in such a way that you can register your client class as a transient as well. Transient means that any time the object is requested it will create a new object of that type. You cannot inject a scoped object into a transient object. You'll run into some weird, misleading exceptions when trying to resolve that object. You can however, put a transient object into a scoped one.

After understanding how the client factory works, you'll find you need to create a client class that does nothing other than communicate with the backend API. This follows the single purpose principal.

Here's an example minimal client class that makes a single Get request:

using System.Net.Http;
using System.Threading.Tasks;

namespace Vecc.LogCorrelation.Example.Source.Services
{
    public class TargetHeadersClient
    {
        private readonly HttpClient _httpClient;

        public TargetHeadersClient(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        public async Task<string> DumpAsync()
        {
            var response = await _httpClient.GetAsync("Headers/Dump");

            response.EnsureSuccessStatusCode();

            var result = await response.Content.ReadAsStringAsync();
            return result;
        }
    }
}

The small class that adds my X-SessionId and Request-Id headers to the outgoing HttpRequestMessage before being sent:

using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace Vecc.LogCorrelation.Example.Source.Services.Internal
{
    public class DefaultRequestIdMessageHandler : DelegatingHandler
    {
        private readonly ISessionIdAccessor _sessionIdAccessor;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public DefaultRequestIdMessageHandler(ISessionIdAccessor sessionIdAccessor, IHttpContextAccessor httpContextAccessor)
        {
            this._sessionIdAccessor = sessionIdAccessor;
            this._httpContextAccessor = httpContextAccessor;
        }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            request.Headers.Add("Request-Id", _httpContextAccessor.HttpContext.TraceIdentifier);
            request.Headers.Add("X-SessionId", _sessionIdAccessor.GetSessionId());

            return base.SendAsync(request, cancellationToken);
        }
    }
}

And now we register everything in the ConfigureServices method of the Startup class:

services.AddTransient<DefaultRequestIdMessageHandler>();
services.AddHttpClient<TargetHeadersClient>((client) => client.BaseAddress = new System.Uri("https://localhost:44324"))
        .AddHttpMessageHandler<DefaultRequestIdMessageHandler>();

Since this is done outside of your client, you can apply it to any HttpClient that is managed by the HttpClientFactory by adding that one extension method to the end.

That's it. Super easy to add those headers or do anything else you want to the request before sending it off.