Skip to content

Month: February 2022

Storing Context Data in C# using AsyncLocal

In procedural and functional programming, the way to pass data between methods is by either a parameter (incoming) or a return value (outgoing). This works, but it has a downside – the state you are passing left and right is now part of every API. One way to get rid of this is by using a Context object that can be accessed from anywhere in the code. And in a multi-threaded, async codebase (which is very popular nowadays), matching between the current flow and the correct context can be tricky. For this purpose, we have the AsyncLocal class.

As written in the docs: “Represents ambient data that is local to a given asynchronous control flow, such as an asynchronous method.” So what exactly does this mean? Let’s take a look.

I’ll start with a simple .net console project (which I create using dotnet new console in a clean directory). In this sample, I’ve written a typed context provider class that does two things: 1) Initializes a context and returns it to the caller, and 2) Returns the current context to the caller. This is how it looks:

public class ContextProvider<T> where T : new()
{
    public T InitContext()
    {
        _asyncLocal.Value = new T();
        return _asyncLocal.Value;
    }

    public T GetContext()
    {
        return _asyncLocal.Value;
    }

    private static readonly AsyncLocal<T> _asyncLocal = new AsyncLocal<T>();
}

Now let’s see how this behaves. I’m going to create a context class that contains a Guid, run a couple of async tasks and see what happens:

Random r = new Random();
var ctxProvider = new ContextProvider<Context>();

async Task InitializeAndPrintContextAsync(int id)
{
    var ctx = ctxProvider.InitContext();
    var guid = Guid.NewGuid();
    Console.WriteLine($"Init context for id: {id} - {guid}.");
    ctx.id = guid;
    await Task.Delay(r.Next(1000));
    ctx = ctxProvider.GetContext();
    Console.WriteLine($"Current context for id: {id} - {ctx.id}.");
}

InitializeAndPrintContextAsync(1);
InitializeAndPrintContextAsync(2);
InitializeAndPrintContextAsync(3);

Console.Read();

public class Context
{
    public Guid id {get; set;}
}

This program executes 3 async tasks in parallel (all three InitializeAndPrintContextAsync tasks run in parallel since they are not awaited). Inside, each one initializes a new context, stores a value in the context, sleeps, and prints the stored value. The magic here is performed by the AsyncLocal class – note that all calls to GetContext() access the same _asyncLocal instance and fetch the same Value property. But for each flow, a different value is fetched:

> dotnet run
Init context for id: 1 - b05b6551-d25f-4fc2-8496-307c0c03725f.
Init context for id: 2 - 8bde25df-0356-453c-be29-afd44f7bdc0b.
Init context for id: 3 - c18954bc-372a-4751-952a-ea4f6e0ae777.
Current context for id: 3 - c18954bc-372a-4751-952a-ea4f6e0ae777.
Current context for id: 1 - b05b6551-d25f-4fc2-8496-307c0c03725f.
Current context for id: 2 - 8bde25df-0356-453c-be29-afd44f7bdc0b.

We can take this a step further and see that this also works if the call is done deeper in the async flow, and adding more data to the context:

Random r = new Random();
var ctxProvider = new ContextProvider<Context>();

async Task InitializeAndPrintContextAsync(int id)
{
    var ctx = ctxProvider.InitContext();
    var guid = Guid.NewGuid();
    Console.WriteLine($"Init context for id: {id} - {guid}.");
    ctx.id = guid;
    await Task.Delay(r.Next(1000));
    ctx = ctxProvider.GetContext();
    Console.WriteLine($"Current context for id: {id} - {ctx.id}.");
    await Level1(id);
}

async Task Level1(int id)
{
    await Task.Delay(r.Next(1000));
    var ctx = ctxProvider.GetContext();
    ctx.name = $"MyName{id}";
    Console.WriteLine($"Level1 - Current context for id: {id} - {ctx.id}.");
    await Level2(id);
}

async Task Level2(int id)
{
    await Task.Delay(r.Next(1000));
    var ctx = ctxProvider.GetContext();
    Console.WriteLine($"Level2 - Current context for id: {id} - {ctx.id}, name {ctx.name}.");
}

InitializeAndPrintContextAsync(1);
InitializeAndPrintContextAsync(2);
InitializeAndPrintContextAsync(3);

Console.Read();

public class Context
{
    public Guid id {get; set;}
    public string name {get; set;}
}

And the output again matches our expectations:

> dotnet run
Init context for id: 1 - 038c4376-abbd-4532-a75e-57433788ae1b.
Init context for id: 2 - 9033ef6d-e05a-49c6-802b-4c384c8829bc.
Init context for id: 3 - 9a27e304-015c-4755-abbf-d18b21bca6e6.
Current context for id: 2 - 9033ef6d-e05a-49c6-802b-4c384c8829bc.
Current context for id: 1 - 038c4376-abbd-4532-a75e-57433788ae1b.
Current context for id: 3 - 9a27e304-015c-4755-abbf-d18b21bca6e6.
Level1 - Current context for id: 2 - 9033ef6d-e05a-49c6-802b-4c384c8829bc.
Level1 - Current context for id: 3 - 9a27e304-015c-4755-abbf-d18b21bca6e6.
Level1 - Current context for id: 1 - 038c4376-abbd-4532-a75e-57433788ae1b.
Level2 - Current context for id: 2 - 9033ef6d-e05a-49c6-802b-4c384c8829bc, name MyName2.
Level2 - Current context for id: 3 - 9a27e304-015c-4755-abbf-d18b21bca6e6, name MyName3.
Level2 - Current context for id: 1 - 038c4376-abbd-4532-a75e-57433788ae1b, name MyName1.

Nothing like magic to get the job done :-).

The code for this post (and for many other things) can be found in my GitHub repo. As always, happy coding!