-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Description
Version Used:
Visual Studio 16.4.2
Steps to Reproduce:
I came across this when trying to leverage the capability to build custom async method return types.
I wanted to build a type similar to Task, but which would be resiliant to StackOverflow. To do this, I wanted to do more inside the methodbuilder's Start() method rather than just always calling stateMachine.MoveNext(). Here's a minimal example which reproduces the behavior I found.
class Program
{
public static void Main(string[] args)
{
Foo(10).GetAwaiter().GetResult(); // throws "no state set"
async MyTask<string> Foo(int n) =>
n <= 0 ? "bar" : await Foo(n - 1);
}
}
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder<>))]
public readonly struct MyTask<TResult> : INotifyCompletion
{
internal MyTask(object state) { }
public bool IsCompleted => false;
public MyTask<TResult> GetAwaiter() => this;
public TResult GetResult() => throw new NotImplementedException();
public void OnCompleted(Action continuation) => throw new NotImplementedException();
}
public struct MyTaskMethodBuilder<TResult>
{
private object _state;
public static MyTaskMethodBuilder<TResult> Create() => default;
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine
{
// doesn't stick because a copy of the builder is used for accessing the Task property
this._state = new State<TStateMachine>();
}
public void SetStateMachine(IAsyncStateMachine stateMachine) { }
public void SetException(Exception exception) => throw new NotImplementedException();
public void SetResult(TResult result) => throw new NotImplementedException();
public void AwaitOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter,
ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine => throw new NotImplementedException();
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
ref TAwaiter awaiter,
ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine => throw new NotImplementedException();
public MyTask<TResult> Task => new MyTask<TResult>(this._state ?? throw new InvalidOperationException("no state set"));
internal object ObjectIdForDebugger => this._state ??= new State<IAsyncStateMachine>();
private class State<TStateMachine>
where TStateMachine : IAsyncStateMachine
{
}
}Expected Behavior:
With the builder being a mutable struct, I would expect that the generated async code would ensure that each call made into the builder is done in such a way that ensures that the builder state can flow between calls. This works between AwaitOnCompleted and Task, for example, but not between Start and Task.
Actual Behavior:
Setting instance fields of the builder in Start() does not carry over to Task.