39

One of the less-talked-about features of C#7 is "generalized async return types", which is described by Microsoft as:

Returning a Task object from async methods can introduce performance bottlenecks in certain paths. Task is a reference type, so using it means allocating an object. In cases where a method declared with the async modifier returns a cached result, or completes synchronously, the extra allocations can become a significant time cost in performance critical sections of code. It can become very costly if those allocations occur in tight loops.

The new language feature means that async methods may return other types in addition to Task, Task<T> and void. The returned type must still satisfy the async pattern, meaning a GetAwaiter method must be accessible. As one concrete example, the ValueTask type has been added to the .NET framework to make use of this new language feature:

That sounds great, but I cannot for the life of my find any example that doesn't just use the stock ValueTask<T> type. I want to make my own Task-like type. Specifically I want a type that behaves like a Task<T>, but with a more functional style of error handling.

Here is the type I am using for functional error handling in my project:

public class Try<T> {
    public T Data { get; }
    public Exception Error { get; }

    public bool HasData => Error == null;
    public bool HasError => Error != null;

    public Try(T data) {
        Data = data;
    }

    public Try(Exception error) {
        Error = error;
    }
}

Here is what I think my custom awaitable type should look like:

public class TryTask<T> : Task<Try<T>> {

    public TryTask(Func<Try<T>> func)
        : base(func) { }

    //GetAwaiter is defined on base type, so we should be okay there
}

This all compiles, until I try to use it as an async return type:

async TryTask<int> DoWhatever() {
    return await new TryTask<int>(() => new Try<int>(1));
}

This method will give the compiler error The return type of an async method must be void, Task, or Task.

How do I make this or something like it compile?


Update:

To confirm, I am using the VS 2017 release from 3/7, and I am able to use other C#7 features in my project, such as local functions.

I have also tried using ValueTask and am getting the same compiler error.

static async ValueTask<int> DoWhatever() {
    return await new ValueTask<int>(1);          
}

Here's another post that sheds some light on whats going on.
How do I get the new async semantics working in VS2017 RC?

Apparently a separate "method builder" type needs to be defined and special attributes need to be applied to the awaitable type. I don't know if I really have time to dig into this. It seems more like metaprogramming hackery than a "language feature".

8
  • 1
    Just to confirm....are any other C# 7 features working in your project? Commented Mar 10, 2017 at 15:29
  • I can make local functions. I am also running 2017 release and targeting .NET 4.6.2 Commented Mar 10, 2017 at 15:30
  • It's not hackery, you're just using an attribute to inform the compiler that the type is to be treated as "task-like". But yes, you'll need to apply the attribute in an appropriate fashion. Commented Mar 10, 2017 at 15:56
  • Well, the attribute must be applied, and it takes a parameter of type Type, which must be some method-builder type. So now I need to define that type. But the problem also arises that I'm trying to create a generic task type, so my method-builder should be generic, but you can't pass genetic types to attribute arguments. Commented Mar 10, 2017 at 16:04
  • 1
    Do you use any additional plugins / extensions except visual Studio itself? Commented Mar 10, 2017 at 16:33

1 Answer 1

31

I couldn't find any good tutorial yet. But you can look at the compiler unittests which create such task-like types (look for "[AsyncMethodBuilder").

The starting point is to create a type and mark it as task-like with an attribute like [AsyncMethodBuilder(typeof(MyTaskBuilder))]. Then you need to define your own MyTaskBuilder type. It must implement a certain pattern (see below). That is the same pattern implemented by the regular AsyncMethodBuilder type which supports regular Task.

class MyTaskBuilder
{
    public static MyTaskBuilder Create() => null;
    public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { }
    public void SetStateMachine(IAsyncStateMachine stateMachine) { }
    public void SetResult() { }
    public void SetException(Exception exception) { }
    public MyTask Task => default(MyTask);
    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine { }
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine { }
}

Update: a small spec for task-like types was added to the compiler documents.

Sign up to request clarification or add additional context in comments.

6 Comments

What are the implementation details of these task-types? I feel like these is some logic expectations that need to be explicitly documented.
@DouglasGaskell See the linked speclet for a bit more details. Also, I would look for resources that explain how AsyncMethodBuilder works.
This answer is really helpful, but the builders used in the AsyncMethodBuilder attributes of the unit tests have empty implementations. I guess they only test that it compiles or that it doesn't. I wish there was a complete example on this... I really want to create my own task-like type, in order to avoid nested-generics-hell... I've decompiled the AsyncTaskMethodBuilder<TResult>, but it uses a lot of internal dependencies from the assembly, which I have no idea what they do...
OK, I just found this, but it seems really simpler than what I see inside the decompiled class: devblogs.microsoft.com/premier-developer/…
If you don't do anything funky with your custom Task-like objects and want them mostly behave like regular tasks, the easiest approach would be just create AsyncTaskMethodBuilder inside your MyTaskBuilder and forward all calls to it
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.