Suppose you have a component (an API controller, for instance) that has a dependency on another component. And you have all of this nicely configured using your favorite IoC (Inversion of Control) library: SimpleInjector. In most applications you also have some cross-cutting concerns like logging, validation or caching. Ideally, you do not want these cross-cutting concerns influence the way you write your business code.

Let’s take a hypothetical situation where you want to correlate log messages across different components. So component A calls component B calls component C and you want to log each call and be able to see that these calls were part of the same operation. You need some correlation id.

Logging and this correlation id have nothing to do with your business logic so you do not want any logging statements in your business code and you definitely do not want to pass a ‘correlation id’ around everywhere. How to solve this?

One possible answer is: AsyncLocal<T>. It allows you to persist data across asynchronous control flows. Besides, the data you store in an AsyncLocal is local to the current asynchronous control flow. So if you have a web application that receives multiple simultaneous requests, each request sees its own async local value.

I illustrated this with a small project on Github. It contains a simple controller that returns a customer by id from some repository. The repository is injected as a dependency into the controller. The controller method also initializes an async local value:

AsyncLocal.SimpleInjector.Web/Controllers/CustomerController.cs view raw
1
2
3
4
5
6
7
8
9
10
11
12
13
[HttpGet]
public async Task<Customer> Get(int id)
{
    // Set async local correlation id.
    var correlationId = Guid.NewGuid();
    _correlationContainer.SetCorrelationId(correlationId);

    // Call controller dependency (decorated by LoggingDecorator).
    var customer = await _customerService.GetCustomer(id);

    // Return values.
    return customer;
}

On lines 5 and 6 I generate a new ‘correlation id’ (but this can be any value or object you like) and set it in a container, which I will show in a minute. Note that this correlation id does not have to be passed in the GetCustomer call on line 9.

The CorrelationContainer is a simple wrapper around an AsyncLocal<Guid>:

AsyncLocal.SimpleInjector.Web/Controllers/CorrelationContainer.cs view raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CorrelationContainer
{
    private readonly AsyncLocal<Guid> _correlationId = new AsyncLocal<Guid>();

    public void SetCorrelationId(Guid correlationId)
    {
        _correlationId.Value = correlationId;
    }

    public Guid GetCorrelationId()
    {
        return _correlationId.Value;
    }
}

This wrapper class is injected as a singleton dependency by Simple Injector so there is only one instance. However, the AsyncLocal takes care of providing each asynchronous control flow (in this example each web request) with its own value.

Finally we have a decorator that does our logging. Log messages should contain the correct correlation id.

AsyncLocal.SimpleInjector.Web/Controllers/LoggingDecorator.cs view raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class LoggingDecorator : ICustomerService
{
    private readonly Func<ICustomerService> _decorateeFunc;
    private readonly ILogger<CustomerService> _logger;
    private readonly CorrelationContainer _correlationContainer;

    public LoggingDecorator(
        Func<ICustomerService> decorateeFunc,
        ILogger<CustomerService> logger,
        CorrelationContainer correlationContainer)
    {
        _decorateeFunc = decorateeFunc;
        _logger = logger;
        _correlationContainer = correlationContainer;
    }

    public async Task<Customer> GetCustomer(int customerId)
    {
        // Get async local correlation id and log.
        var correlationIdBeforeAwait = _correlationContainer.GetCorrelationId();
        _logger.LogWarning($"Getting customer by id {customerId} ({correlationIdBeforeAwait})");

        // Call decoratee.
        var decoratee = _decorateeFunc.Invoke();
        var customer = await decoratee.GetCustomer(customerId);

        // For demo purposes: get correlation id again after await and log.
        var correlationIdAfterAwait = _correlationContainer.GetCorrelationId();
        _logger.LogWarning($"Retrieved customer by id {customerId} ({correlationIdBeforeAwait})");

        // Return values.
        return customer;
    }
}

It looks at the same singleton CorrelationContainer instance that CustomerController used for context information and logs some messages before and after calling the decoratee. Example log messages:

Note that the same correlation id is logged before and after the await in LoggingDecorator. And not that nowhere did we have to pass this correlation id as a parameter in our business APIs.

And as a final note, I used SimpleInjector to illustrate the usage of AsyncLocal in a decorator but you can use this in many more situation of course.