Razzie
Razzie

Reputation: 31222

Await the result of Task<TDerived> using reflection in a non-generic method

Consider the following case:

class A
{
    public int Id;
}

class B : A
{

}

class Main
{
    public async Task<int> Create(Type type)
    {
        MethodInfo method = this.GetType().GetMethod("Create", new Type[] { typeof(string) }).MakeGenericMethod(new Type[] { type });
        A a = await (Task<A>)method.Invoke(this, new object[] { "humpf" });
        return a.Id;
    }

    public async Task<T> Create<T>(string name) where T : A
    {
        T t = await Foo.Bar<T>(name);
        return t;
    }
}

Calling new Main().Create(typeof(B)) will fail with a

Unable to cast object of type 'System.Threading.Tasks.Task[B]' to type 'System.Threading.Tasks.Task[A]'

I don't quite understand because in this case, the Generic Create<T> method can only return a Task<T> where T is always derived from 'A', but maybe I'm missing an edge case here. Besides that, how can I make this work? Thanks!

Upvotes: 13

Views: 5170

Answers (4)

Gennadii Saltyshchak
Gennadii Saltyshchak

Reputation: 637

My option has form of extension method. It takes any instance of Task and returns Task<object?> with actual result for generic tasks or null for non-generic ones.

internal static class TaskExtensions
{
    public static async Task<object> ToGenericTaskAsync(this Task task)
    {
        await task;
        var taskType = task.GetType();
        if (!IsAssignableToGenericTaskType(taskType))
        {
            return null;
        }

        return task
            .GetType()
            .GetProperty("Result", BindingFlags.Instance | BindingFlags.Public)!
            .GetValue(task);
    }

    private static bool IsAssignableToGenericTaskType(Type type)
    {
        if (type.IsGenericType &&
            type.GetGenericTypeDefinition() == typeof(Task<>) &&
            type.GetGenericArguments()[0] != Type.GetType("System.Threading.Tasks.VoidTaskResult"))
        {
            return true;
        }

        return type.BaseType is not null && IsAssignableToGenericTaskType(type.BaseType);
    }
}

Usage:

Task<object?> t1 = await Task.CompletedTask.ToGenericTaskAsync();
Task<object?> t2 = await Task.FromResult<int>(5).ToGenericTaskAsync();

Example in .NET Fiddle

Upvotes: 0

onets
onets

Reputation: 91

I needed to get task result in Castle Interceptor. This code works for me:

if (invocation.ReturnValue is Task task)
{
    task.Wait();

    var result = invocation.ReturnValue.GetType().GetProperty("Result").GetValue(task);
    _cacheProvider.Set(_key, result, _duration);
}

Upvotes: 0

LawMan
LawMan

Reputation: 3625

The above solution really helped me. I made a small tweak to the @Lukazoid solution...

var resultProperty = typeof(Task<>).MakeGenericType(type).GetProperty("Result");
A a = (A)resultProperty.GetValue(task);

to

dynamic a = task.GetType().GetProperty("Result")?.GetValue(task);

Upvotes: -1

Lukazoid
Lukazoid

Reputation: 19416

As per my comment:

Unlike interfaces, concrete types such as Task<TResult> cannot be covariant. See Why is Task not co-variant?. So Task<B> cannot be assigned to a Task<A>.

The best solution I can think of is to use the underlying type Task to perform the await like so:

var task = (Task)method.Invoke(this, new object[] { "humpf" });
await task;

Then you can use reflection to get the value of the Result:

var resultProperty = typeof(Task<>).MakeGenericType(type).GetProperty("Result");
A a = (A)resultProperty.GetValue(task);
return a.Id;

Upvotes: 28

Related Questions