Get the result of Func<object> when object is a Task<Something>
Asked Answered
A

1

3

I am currently using this code to attempt to dynamically execute a saved Func<object>:

public async Task<object> GetFuncResult(string funcName) {
    Func<object> func = _savedFuncs[funcName];
    bool isAwaitable = func.Method.ReturnType.GetMethod(nameof(Task.GetAwaiter)) != null;
    if (!isAwaitable) return func();
    else return await ((Func<Task<object>>)func)();
}

If somebody stores a Func<Task<object>> or a Func<[anything]> this code works fine. But if somebody stores a Func<Task<string>> (or any other generic argument in the Task), it breaks.

Unable to cast object of type Func<Task<System.String>> to type Func<Task<System.Object>>

My question is: How do I await the result of the Func<Task<Something>> at this point and just return that value as an object?

Full Test Code:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace TestConsole
{
    class Program
    {
        static Dictionary<string, Func<object>> _savedFuncs;

        static async Task Main(string[] args)
        {
            _savedFuncs = new Dictionary<string, Func<object>>();
            Func<Task<string>> myTask = async () => { return "Test Success"; };
            _savedFuncs.Add("myFunc", myTask);
            Console.WriteLine((await GetFuncResult("myFunc")) ?? "No Value Returned");
            Console.ReadKey();
        }

        public static async Task<object> GetFuncResult(string funcName)
        {
            Func<object> func = _savedFuncs[funcName];
            bool isAwaitable = func.Method.ReturnType.GetMethod(nameof(Task.GetAwaiter)) != null;
            if (!isAwaitable) return func();
            return await ((Func<Task<object>>)func)();
        }
    }
}
Aspirator answered 28/11, 2019 at 0:30 Comment(9)
Why did you put Object as type in a generic? Not using object as type is literally what generics were invented/implemented for. Without knowing wich specific return values those tasks might have, there is not much we can help you with.Matthews
@Matthews I appreciate that question. These funcs can return literally any object. This is essentially a Composition Root which allows a user to register a Function that represents an object, which could literally be anything. Later they can get that object out as-necessary.Aspirator
then do it it literally Task<T> GetFuncResult<T>(string funcName)Penetralia
@Penetralia You're not wrong for this example. The (only slightly) more complex real code requires a few signatures of flexibilty for the user. Most of them ARE Generic like GetFuncResult<T>(...), and your suggestion would work. But a couple of them are like GetFuncResult(Type type, ...), and that wouldn't work. As much as I wish it were that simple. I need the demonstrated non-generic method signature to work.Aspirator
Does this answer your question? #14712085Evin
Or rather this? #39675488Evin
@Evin I know the path you're thinking here. I don't jump to reflection unless it's unavoidable. But I can imagine getting a Func, grabbing the Method of the Func, and executing it. Given that such high-point peeps don't have an immediate non-reflection answer makes me lose hope a bit. But if I have to go the reflection route, I can handle that just fine. Still crossing my fingers though.Aspirator
There's nothing wrong with reflection in a lot of situations, this may be one of them. Also worth noting that reflection is internally cached, so your performance will possibly still be pretty good.Evin
@Evin I do agree when necessary. I implemented some reflection and it worked, but Peter's new answer seems cleaner to me. If you did downvote him, perhaps his changes may prompt you to un-downvote. Thanks for your time!Aspirator
C
2

I'm not entirely clear on what your intent here is, because the code is not clear. You are looking for a GetAwaiter() method on the return type, but of course there are types other than Task which have this method. Also, your code will miss things that are awaitable by virtue of extension method.

If you are going to assume the function returns a task object (which the code currently does), then you should just check for that instead of the GetAwaiter() method. Conversely, you should just invoke the GetAwaiter() method dynamically, so that anything with the method is accommodated.

Personally, if this code isn't going to be invoked very often, I would use dynamic, try to call GetAwaiter(), catch the exception if that fails (because the method isn't present) and just invoke the delegate directly. If perf is important, you can memoize the type-to-awaiter status so that the exception can be skipped after you hit it once. Note that using dynamic, you'll accommodate most awaitable scenarios (it still won't find extension-method GetAwaiter()s).

Here is an example of that:

private static readonly HashSet<MethodInfo> _notAwaitable = new HashSet<MethodInfo>();

public static async Task<object> GetFuncResult(string funcName)
{
    Func<object> func = _savedFuncs[funcName];
    dynamic result = func();

    if (!_notAwaitable.Contains(func.Method))
    {
        try
        {
            return await result;
        }
        catch (RuntimeBinderException) { } // not awaitable

        _notAwaitable.Add(func.Method);
    }

    return result;
}

This should do what you want, and should also be performant. The dynamic runtime support already caches the resolution of the awaitable scenario, and by storing the non-awaitable MethodInfo instances in the hash set, the code avoids ever suffering the RuntimeBinderException more than once for any given delegate target method.

Once the code's "warmed up" (i.e. has been invoked in the way it will on subsequent passes), it should not be a bottleneck.

Note that the implementation above assumes you are not using multicast delegates. Given the context, that seems like a reasonable assumption, since there's no built-in language support for awaitable multicast delegates (or rather, it will work, but nothing in the runtime resolves the ambiguity as to which awaitable is awaited). But you could of course extend the above to support multicast delegates if you really needed that.

If you don't care about supporting all awaitable scenarios, but only Task-based ones, you can simplify the above a bit like this:

public static async Task<object> GetFuncResult(string funcName)
{
    Func<object> func = _savedFuncs[funcName];
    object result = func();

    if (result is Task task)
    {
        await task;
        return ((dynamic)task).Result;
    }

    return result;
}

Here, the type-check for Task is used in lieu of the hash set. Again, the dynamic runtime support will cache the Result accessor for each type of task used with this method, so once warmed up will perform just as well as any other dynamically-oriented solution.

Finally, note that if you have a Func<Task>, the above won't work, because it assumes all Task objects have a valid result. One might argue that given the ambiguity, it'd be best to not populate the dictionary with anything like that in the first place. But assuming that scenario is of concern, the above can be modified to account for the possibility:

public static async Task<object> GetFuncResult(string funcName)
{
    Func<object> func = _savedFuncs[funcName];
    object result = func();

    if (result is Task task)
    {
        Type resultType = result.GetType();

        // Some non-result task scenarios return Task<VoidTaskResult> instead
        // of a plain non-generic Task, so check for both.
        if (resultType != typeof(Task) &&
            resultType.GenericTypeArguments[0].FullName != "System.Threading.Tasks.VoidTaskResult")
        {
            await task;
            return ((dynamic)task).Result;
        }
    }

    return result;
}

Unfortunately, because in some cases the compiler uses Task<VoidTaskResult> instead of the non-generic Task type, it's not sufficient to just check for Task. In addition, because VoidTaskResult is not a public type, the code has to check for the type name as a string value instead of typeof(Task<VoidTaskResult>). So, it's a little awkward. But it will address the scenarios where the thing being returned is the Task itself, not the result of the task.

Of course, the GetAwaiter() approach doesn't have this issue. So if this was actually of concern, that would be one reason to choose the GetAwaiter() approach instead of the is Task approach.

Comminate answered 28/11, 2019 at 0:44 Comment(8)
You both type too quickly for me to get some words out. This is not in a millions-per-second loop or anything, but every time an entry-point is hit, this method will probably be hit anywhere from 5-20 times. Not a huge deal really. I like some of the ideas mentioned here, and I did see that the code example pseudo code didn't quite compile, but I am taking some of this into account and attempting to come up with how I could use it. Though I am very familiar with everything you said, I can't quite grasp this niche use case I have and how to use these concepts quite yet.Aspirator
Peter, I like both of those code examples. I did find a way to make it work with reflection, but backed off, and I am equally happy and standoffish regarding dynamic, but I think I'm going to stick with your dynamic examples. They actually tested well and work as I expect. Thanks!Aspirator
unless someone put Func<Task>Penetralia
Be warned that dynamic can be brutally slow. I'd suggest you benchmark this.Evin
@DavidG: my experience has been that the DLR caching addresses the worst of the performance hits. I wouldn't use dynamic in a tight loop, but in most other scenarios it works well. Still, the advice to benchmark is always valid for any performance-critical areas of the code. In any case, I don't see any way around the underlying potential performance issue; if the method is to determine at run-time what should happen, that necessarily involves some kind of dynamic code, whether dynamic itself or reflection-based techniques that do the same thing but more explicitly.Comminate
@Selvin: you are right. I'd assumed that the only tasks present would be those that return values async, and maybe that was a valid assumption. But I've gone ahead and added a version for the scenario where one only cares about tasks, but could have tasks without results returned by delegates in the dictionary.Comminate
Yeah, I was just thinking that it might be quicker to use reflection rather than dynamic to get .Result.Evin
Oh, and this would also need updating to cope with ValueTask, but that might be outside of the scope for OP.Evin

© 2022 - 2024 — McMap. All rights reserved.