Skip to main content

Use AsyncLocal with SimpleInjector for unobtrusive context

· 3 min read

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 to 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
loading...

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
loading...

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
loading...

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:

Log correlation id

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.