Awaiting a reflected method where the return type is only known at runtime

667 views Asked by At

I have a bunch of classes that include an async method ExecuteAsync().

These classes return different types.

I need to invoke the ExecuteAsync() method by reflection.

I know, from many other questions on SO, that if I knew the return type, I could just write, for example:

var task (Task<int>) obj.GetType().GetMethod("ExecuteAsync").Invoke(obj, null);
var result = await task;

I've got to the point where I can do this to get the task...

    var method = p.GetType().GetMethod("ExecuteAsync");
    var rt = method.ReturnType.Dump();
    var task = Convert.ChangeType(method!.Invoke(p, null)!, rt);

but then var result = await task won't compile as the compiler thinks task is of type object? and "object? is not awaitable".

Is there a way of executing/awaiting task?

Am I going about this the wrong way?

3

There are 3 answers

0
JonasH On BEST ANSWER

If you start to use reflection you tend to need to continue using reflection. You should be able to cast your result to a Task and await this. But to get the actual result you probably need to use reflection to access the Task.Result property. So I would try something like this:

var resultTask = obj.GetType().GetMethod("ExecuteAsync").Invoke(obj, null);
await (Task)resultTask ;
var result = (int)resultTask.GetType().GetProperty("Result").GetValue(resultTask, null);

But I would only use solutions like this as the last resort, or for very quick and dirty code. In most cases other patterns can be used, and should be easier to maintain than using reflection.

0
Dai On
  • Ultimately your problem is getting the .Result of a Task<T> from a T-which-we-don't-know to a boxed value (i.e. as an Object reference).
  • One option is to use a normal generic method to await the real Task, and return the result as an Object?.
    • ...and to invoke that "normal" generic method via MakeGenericMethod which allows you to specify generic type arguments at runtime using reflection.

Like so:

  • Utility.InvokeAndAwaitReflectedMethodImplAsync is the generic method which is instantiated as a closed generic method using MakeGenericMethod using type-arguments derived from MethodInfo.
  • class Mystery's DoSomethingAsync method is being invoked using reflection, without any part of the program being-aware (at compile-time) that DoSomethingAsync returns Task<Int32>.
class Mystery
{
    private readonly Random rng = new Random();
    
    public async Task<Int32> DoSomethingAsync()
    {
        await Task.Delay( 1000 );
        
        return this.rng.Next();
    }
}

public static async Task Main()
{
    Object mysteryObject = new Mystery();

    Type typeOfMystery = mysteryObject.GetType();

    MethodInfo methodInfo = typeOfMystery.GetMethod( "DoSomethingAsync" );
    
    //

    Object? result = await Utility.InvokeAndAwaitReflectedMethodAsync( mysteryObject, methodInfo, Array.Empty<Object?>() );
   
    Console.WriteLine( "Result: {0}", result );
}

public static class Utility
{
    private static readonly MethodInfo _invokeAndAwaitMethodInfo = typeof(Utility).GetMethod( name: nameof(InvokeAndAwaitReflectedMethodImplAsync), BindingFlags.NonPublic | BindingFlags.Static )!;

    public static async Task<Object?> InvokeAndAwaitReflectedMethodAsync( Object obj, MethodInfo methodInfo, Object?[] args )
    {
        Type methodTaskInnerType = methodInfo.ReturnType.GetGenericArguments()[0]; // Assuming `methodInfo.ReturnType == Task<T>`, then `methodTaskInnerType == T`.
        
        //
        
        MethodInfo invokerGen = _invokeAndAwaitMethodInfo.MakeGenericMethod (/*TObject:*/ obj.GetType(), /*TReturn:*/ methodTaskInnerType );

        Object[] argsForInvokeAndAwait = new Object[] { /*obj:*/ obj, /*methodInfo:*/ methodInfo, /*args:*/ args };

        Task<Object?> asTaskObj = (Task<Object?>)invokerGen.Invoke( obj: null, parameters: argsForInvokeAndAwait )!;

        Object? result = await asTaskObj;
        return result;
    }
    
    private static async Task<Object?> InvokeAndAwaitReflectedMethodImplAsync<TObject,TReturn>( TObject obj, MethodInfo methodInfo, Object?[] args )
    {
        Type typeOfTaskOfT = typeof(Task<TReturn>);
        if( methodInfo.ReturnType != typeOfTaskOfT )
        {
            String msg = "ReturnType does not match T. Expected: " + typeOfTaskOfT.FullName + ", Actual: " + methodInfo.ReturnType.FullName;
            throw new ArgumentException( message: msg, paramName: nameof(methodInfo) );
        }

        Object? taskObj = methodInfo.Invoke( obj: obj, parameters: args );
        if( taskObj is null )
        {
            throw new InvalidOperationException( "Method returned null. Expected Task." );
        }
        else if( taskObj is Task<TReturn> task )
        {
            TReturn value = await task.ConfigureAwait(false);
            Object? valueAsObject = value;
            return valueAsObject;
        }
        else
        {
            throw new InvalidOperationException( "Unexpected return value." );
        }
    }
}

Screenshot proof in Linqpad:

enter image description here

0
Paulo Morgado On

dynamic is often abused, but it can be used here:

dynamic dyn = obj;
var result = (int)(await dyn.ExecuteAsync());
dynamic dyn = obj;
Task<int> task = dyn.ExecuteAsync();
var result = await task;