0

I have a method that returns an object that can be any type. If the object instance is awaitable, I need to await it and get its result. I know I can check for the object to be a Task/ValueTask or their generic counterparts, but those are not the only things that are awaitable.

object result = GetResult();

object actualResult = result is Awaitable awaitable // "Awaitable" isn't a thing
    ? await awaitable
    : result;

The C# language specification says that an expression t is awaitable if one of the following holds:

  • t is of compile-time type dynamic
  • t has an accessible instance or extension method called GetAwaiter with no parameters and no type parameters, and a return type A for which all of the following hold:
    • A implements the interface System.Runtime.CompilerServices.INotifyCompletion
    • A has an accessible, readable instance property IsCompleted of type bool
    • A has an accessible instance method GetResult with no parameters and no type parameters

What is the best way to determine whether an object is awaitable or not?

Other answers to questions similar to this suggest that it's not possible. That can't be true since the compiler knows what is and isn't awaitable. I'm looking for some framework-level methods to determine an object's awaitability.

Another practical example of this takes place in ASP.NET MVC Controllers. Controller methods can return IActionResult or Task<IActionResult> and the code that executes these actions knows whether or not to await the result objects. I can't find the location in the source code where this check happens though.

6
  • 3
    This question is similar to: Is it possible to tell if an object is awaitable at runtime?. If you believe it’s different, please edit the question, make it clear how it’s different and/or how the answers on that question are not helpful for your problem. Commented Jul 9 at 19:40
  • @ipodtouch0218 It is similar. I have revised my question to be more specific.
    – Dan
    Commented Jul 9 at 20:19
  • "Controller methods can return IActionResult or Task<IActionResult> and the code that executes these actions knows whether or not to await the result objects" isn't that the explicit Task return type's doing, not some is Awaitable check? Commented Jul 9 at 20:44
  • @ipodtouch0218 Say some endpoint path "/api/myendpoint" maps to a method in a controller "MyMethod". MyMethod can either return a Task<IActionResult> or an IActionResult. The part of the framework code that maps that path to a method, knows whether or not to await the result of the method call via reflection. I would like to see that code so I can make a similar decision.
    – Dan
    Commented Jul 9 at 20:54
  • 2
    you could probably get reflection to help you to a point, but then you can't really await, you would just have to use GetAwaiter().GetResult(). Is that what you want? Commented Jul 9 at 22:41

1 Answer 1

1

I have settled on the following solution for now. It does not work with types that are only awaitable via extension methods.

public static class Awaitable
{
    private static readonly ConcurrentDictionary<Type, bool> AwaitableTypes = [];
    private static readonly Type[] NoParameters = [];

    public static async Task<object> Create(object obj)
    {
        if (obj is null)
        {
            return null;
        }

        var type = obj.GetType();
        if (type.IsGenericType)
        {
            type = type.GetGenericTypeDefinition();
        }

        if (AwaitableTypes.GetOrAdd(type, IsAwaitable))
        {
            return await (dynamic)obj;
        }

        return obj;
    }

    private static bool IsAwaitable(Type type) =>
        type.GetMethod("GetAwaiter", NoParameters) is MethodInfo getAwaiterMethod &&
        getAwaiterMethod.GetGenericArguments().Length == 0 &&
        getAwaiterMethod.ReturnType.GetInterface("INotifyCompletion") is not null &&
        getAwaiterMethod.ReturnType.GetProperty("IsCompleted") is PropertyInfo isCompletedProperty &&
        isCompletedProperty.PropertyType == typeof(bool) &&
        getAwaiterMethod.ReturnType.GetMethod("GetResult", NoParameters) is MethodInfo getResultMethod &&
        getResultMethod.GetGenericArguments().Length == 0;
}

It takes any object and turns it into a Task<object>.

Example Usage:

object result;
result = await Awaitable.Create(1);
result = await Awaitable.Create("string");
result = await Awaitable.Create(ValueTask.FromResult(Math.PI));
result = await Awaitable.Create(Task.Run(async () =>
{
    await Task.Delay(1000);
    return 42;
}));

Not the answer you're looking for? Browse other questions tagged or ask your own question.