ExecutionContext does not flow up the call stack from async methods

Is this a bug / missing feature or an intentional design decision?

It's an intentional design decision. Specifically, the async state machine sets the "copy on write" flag for its logical context.

A correlation of this is that all synchronous methods belong to their closest ancestor async method.

Do I need to worry about this behavior while writing code that depends on AsyncLocal? Say, I want to write my TransactionScope-wannabe that flows some ambient data though await points. Is AsyncLocal enough here?

Most systems like this use AsyncLocal<T> combined with an IDisposable pattern that clears the AsyncLocal<T> value. Combining these patterns ensures it will work with either synchronous or asynchronous code. AsyncLocal<T> will work fine by itself if the consuming code is an async method; using it with IDisposable ensures it will work with both async and synchronous methods.

Are there any other alternatives to AsyncLocal and CallContext.LogicalGetData / CallContext.LogicalSetData in .NET when it comes down to preserving values throughout the "logical code flow"?

No.


This seems like an intentional decision to me.

As you already know, SetValueInAsyncMethod gets compiled into a state-machine that implicitly captures the current ExecutionContext. When you change the AsyncLocal-variable, that change does not get "flowed" back to the calling function. In contrast, SetValueInNonAsyncMethod is not async and therefore not compiled into a state-machine. Therefore the ExecutionContext is not captured and any changes to AsyncLocal-variables are visible to the caller.

You can capture the ExecutionContext yourself as well, if you need this for any reason:

private static Task SetValueInNonAsyncMethodWithEC()
{
    var ec = ExecutionContext.Capture(); // Capture current context into ec
    ExecutionContext.Run(ec, _ => // Use ec to run the lambda
    {
        asyncLocal.Value = 3;
        PrintValue();
    });
    return Task.CompletedTask;
}

This will output a value of 3, while the Main will output 2.

Of course it is way easier to simply convert SetValueInNonAsyncMethod to async to have the compiler do this for you.

With regards to code that uses AsyncLocal (or CallContext.LogicalGetData for that matter), it is important to know that changing the value in a called async method (or any captured ExecutionContext) will not "flow back". But you can of course still access and modify the AsyncLocal as long as you do not reassign it.