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.