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.