First, some context (pardon the pun). Consider the following two async methods:
public async Task Async1() {
PreWork();
await Async2();
PostWork();
}
public async Task Async2() {
await Async3();
}
Thanks to the async
and await
keywords, this creates the illusion of a nice simple call stack:
- Async1
- PreWork
- Async2
- Async3
- PostWork
But, if I understand correctly, in the generated code the call to PostWork
is tacked on as a continuation to the Async2
task, so at runtime the flow of execution is actually more like this:
- Async1
- PreWork
- Async2
- Async3
- PostWork
- Async3
(and actually it's even more complicated than that, because in reality the use of async
and await
causes the compiler to generate state machines for each async method, but we might be able to ignore that detail for this question)
Now, my question: Is there any way to flow some sort of context through these auto-generated continuations, such that by the time I hit PostWork
, I have accumulated state from Async1
and Async2
?
I can get something similar to what I want with tools like AsyncLocal and CallContext.LogicalSetData, but they aren't quite what I need because the contexts are "rolled back" as you work your way back up the async chain. For example, calling Async1
in the following code will print "1,3", not "1,2,3":
private static readonly AsyncLocal<ImmutableQueue<String>> _asyncLocal = new AsyncLocal<ImmutableQueue<String>>();
public async Task Async1() {
_asyncLocal.Value = ImmutableQueue<String>.Empty.Enqueue("1");
await Async2();
_asyncLocal.Value = _asyncLocal.Value.Enqueue("3");
Console.WriteLine(String.Join(",", _asyncLocal.Value));
}
public async Task Async2() {
_asyncLocal.Value = _asyncLocal.Value.Enqueue("2");
await Async3();
}
I understand why this prints "1,3" (the execution context flows down to Async2
but not back up to Async1
) but that isn't what I'm looking for. I really want to accumulate state through the actual execution chain, such that I'm able to print "1,2,3" at the end because that was the actual way in which the methods were executed leading up to the call to Console.WriteLine
.
Note that I don't want to blindly accumulate all state across all async work, I only want to accumulate state that is causally related. In this scenario I want to print "1,2,3" because that "2" came from a dependent (awaited) task. If instead the call to Async2
was just a fire-and-forget task then I wouldn't expect to see "2" because its execution would not be in the actual chain leading up to the Console.WriteLine
.
Edit: I do not want to solve this problem just passing around parameters and return values because I need a generic solution that will work across a large code base without having to modify every method to pass around this metadata.