Late to the party, but I just published a lib which does accomplish this.
https://github.com/avonwyss/bsn.AsyncLambdaExpression
It leverages the fact that nested Lambdas in an Expression Tree can access the outer lambda variables (which are captured as closure, just like nested lambdas behave in C#). Thus the code creates a main lambda containing all the necessary variables and returning a Task
, and a state machine in a nested lambda which is callable as continuation on awaiter.OnComplete()
. Since the state, current awaiter etc. is stored in the closure, the state machine keeps its state even when the inner lambda stops execution.
The API to use it consists of two extension methods, one for awaiting and one for converting the Expression Tree with await to a lambda implementing the state machine and returning the Task
.
Here's a code sample:
// Build a normal expression tree with "await" calls
var paraInput = Expression.Parameter(typeof(string), "input");
var exprTree = Expression.Lambda<Func<Task<string>, string>>(
Expression.Block(
Expression.Call(typeof(Task), nameof(Task.Delay), null, Expression.Constant(1000)).Await(false),
paraInput.Await(false)),
paraInput);
// Create compilable state machine async expression tree (result must be Task<?> or Task)
var asyncExprTree = exprTree.Async<Func<Task<string>, Task<string>>>();
var asyncCompiled = asyncExprTree.Compile();
// Invoke delegate as usual
var result = await asyncCompiled(Task.FromResult("test")).ConfigureAwait(false);
The DebugView of the original lambda Expression Tree is like this:
.Lambda #Lambda1<System.Func`2[System.Threading.Tasks.Task`1[System.String],System.String]>(System.Threading.Tasks.Task`1[System.String] $input)
{
.Block() {
.Call bsn.AsyncLambdaExpression.AsyncExpressionExtensions.AwaitVoid(.Call (.Call System.Threading.Tasks.Task.Delay(1000)).ConfigureAwait(False));
.Call bsn.AsyncLambdaExpression.AsyncExpressionExtensions.AwaitResult(.Call $input.ConfigureAwait(False))
}
}
The async conversion creates the following Expression Tree:
.Lambda #Lambda1<System.Func`2[System.Threading.Tasks.Task`1[System.String],System.Threading.Tasks.Task`1[System.String]]>(System.Threading.Tasks.Task`1[System.String] $input)
{
.Block(
System.Runtime.CompilerServices.ConfiguredTaskAwaitable+ConfiguredTaskAwaiter $awaiter,
System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1+ConfiguredTaskAwaiter[System.String] $awaiter,
System.Int32 $state,
System.Int32 $resumeState,
System.Threading.Tasks.TaskCompletionSource`1[System.String] $taskCompletionSource,
System.Action $continuation,
System.Exception $exception) {
$taskCompletionSource = .New System.Threading.Tasks.TaskCompletionSource`1[System.String](.Constant<System.Threading.Tasks.TaskCreationOptions>(RunContinuationsAsynchronously));
$continuation = .Lambda #Lambda2<System.Action>;
.Invoke $continuation();
$taskCompletionSource.Task
}
}
.Lambda #Lambda2<System.Action>() {
.Try {
.Loop {
.Switch ($state) {
.Case (0):
.Block() {
$state = 1;
.If (
!($awaiter = .Call (.Call (.Call System.Threading.Tasks.Task.Delay(50)).ConfigureAwait(False)).GetAwaiter()).IsCompleted
) {
.Block() {
.Call $awaiter.OnCompleted($continuation);
.Break :break { }
}
} .Else {
.Default(System.Void)
}
}
.Case (1):
.Block() {
$state = 2;
.Call $awaiter.GetResult();
.If (
!($awaiter = .Call (.Call $input.ConfigureAwait(False)).GetAwaiter()).IsCompleted
) {
.Block() {
.Call $awaiter.OnCompleted($continuation);
.Break :break { }
}
} .Else {
.Default(System.Void)
}
}
.Case (2):
.Block(System.String $result:2) {
$result:2 = .Call $awaiter.GetResult();
.Call $taskCompletionSource.SetResult($result:2);
.Break :break { }
}
.Default:
.Throw .New System.InvalidOperationException()
}
}
.LabelTarget :break:
} .Catch (System.Exception $ex) {
.Call $taskCompletionSource.SetException($ex)
}
}
This is a simple example; but the async conversion can not only handle await, but also the control flow expressions (loop, conditional, label/goto, try..catch..finally) in order to offer support for a wide variety of Expression Trees.