Sharing context data between parent and child tasks

I would like to share some context data that remains for the entire life of a thread/task that set that context data.

Looking at the pseudo code below, I have multiple tasks that all call asynchronous methods. One way or another, they are all child tasks of the 'main', parent, task.

task1 would set context data that would be available to all the children of that task and task2 would do the same but all its children would have access to its data.

tasks (add) task1
               Set context data (value = "1")
               (await) -->  task1-B (await) -->  task1-C (await) -->  thread1-D --|
               |------------------------------------------------------------------|
               UnSet context data (value = null)

tasks (add) task2
               Set context data (value = "2")
               (await) -->  task2-B (await) -->  task2-C (await) -->  thread2-D --|
               |------------------------------------------------------------------|
               UnSet context data (value = null)

await tasks.WaitAll();

What I could do is pass the value from methods to methods, always insuring that the 'context' is passed.

The problem with that approach is that it would mean re-writing all the children methods, (the methods B, C, D in my example).

The other issue is that some methods are called by other method, for example, method E could call method C

task2-E (await) -->  task2-C (await) --|
|--------------------------------------|

In the case above the context might be set already, (re-entry somehow), or not be set at all.

I know a 'child' task cannot get it's parent task or a parent cannot get its children tasks, otherwise I could set some global list that would somehow keep track of all the contexts.

I was just wondering if it was possible to have a 'context' unique to as task and subtasks.


Solution 1:

You're looking for AsyncLocal<T>. A couple of AsyncLocal<T> caveats from my blog:

  1. It should be set in an async method, because that triggers the "copy on write" behavior of the logical thread context.
  2. You should only store immutable data.

Generally, the code I use ends up looking something like this:

internal static class MyAsyncContext
{
  // string is immutable
  private static AsyncLocal<string> _asyncLocal = new();

  // this should ONLY be called from an async method
  public IDisposable Set(string value)
  {
    var previous = _asyncLocal.Value;
    _asyncLocal.Value = value;
    return Disposable.Create(() => _asyncLocal.Value = previous);
  }

  public string? TryGet() => _asyncLocal.Value;
}

(this is using a Disposable helper from my Disposables library)

Usage in parent method:

async Task task1()
{
  using var context = MyAsyncContext.Set("1");
  await task1B();
}

Usage in child method:

async Task task1D()
{
  var contextData = MyAsyncContext.TryGet();
}