2

I'm extending an existing Rpc library to support async methods.

In the current implementation, I'm using factory methods to build typed delegates, like this (I'm leaving out some implementation details, unrelated to the question):

    public static Func<ServiceType, PayloadType, object> BuildServiceMethodInvocation<ServiceType>(MethodInfo serviceMethod)
    {
        return (service, payload) =>
        {
            try
            {
                // payload is mapped onto an object[]
                var parameters = ReadPayload(payload)
                var result = serviceMethod.Invoke(service, parameters);

                return result;
            }
            catch (TargetInvocationException e)
            {
                // bla bla bla handle the error
                throw;
            }
        };
    }

The resulting object is then passed on to a serialization class, which will handle every case.

Now I want to support also asynchronous methods, that is, methods that return a Task<T>.

I want to change the signature of BuildServiceMethodInvocation so that it's a Func<ServiceType, PayloadType, Task<object>>.

I don't know how to easily wrap every possible value in a Task<object>: it should handle plain values, but also Task<T> (boxing into an object).

Any ideas?

Edit: serviceMethod could return a string, and I want to get a Task<object> returning the string value. But serviceMethod could return a Task<int>, in which case I would like to get back a Task<object> returning the int value (boxed into an object).

Alberto Chiesa
  • 7,022
  • 2
  • 26
  • 53
  • If no async is needed: use Task.FromResult(yourObject). – Evk Mar 03 '17 at 13:41
  • AFAICT, Task.FromResult is not going to wrap a result of type Task . – Alberto Chiesa Mar 03 '17 at 13:43
  • Why do you need to wrap Task? You can just return it? – Evk Mar 03 '17 at 13:44
  • The external code must await on a task. How do I know how to wait? How do I return it? See my edit, if it can better explain the problem. – Alberto Chiesa Mar 03 '17 at 13:48
  • Why return an object if what you want is a string anyway? – DavidG Mar 03 '17 at 13:48
  • 1
    Ok I see what you mean. See here for how to do that: http://stackoverflow.com/a/15530170/5311735 – Evk Mar 03 '17 at 13:52
  • @DavidG because the method to be invoked could really be ANY method, with ANY signature. So I was leaving the value returned by Invoke as is (the only processing I do after this invocation is a generic serialization, which takes an object anyway). I don't want a string: I want an object that can be serialized, so I have to "unpack" Task objects. – Alberto Chiesa Mar 03 '17 at 13:55
  • But if you make your method generic (e.g. `BuildServiceMethodInvocation(...)`) then you could specify without the boxing. – DavidG Mar 03 '17 at 13:56
  • Anyway, why do you even need to change this method? For an async method, the returned object will be a `Task`. Your calling code should already be aware what to do with it (i.e. `await` it) – DavidG Mar 03 '17 at 14:08
  • DavigG, serviceMethod.Invoke may return any object, as well as any Task. OP wants to wrap both regular objects to Task(can be done with Task.FromResult), BUT also if serviceMethod.Invoke returned Task > convert that to Task without wrapping in another Task (as Task.FromResult would do). `serviceMethod.Invoke` returns plain `object` as I understand. – Evk Mar 03 '17 at 14:10
  • So you figured out how to do that with my link above? – Evk Mar 03 '17 at 15:03
  • @Evk the link is useful, but it's not simple to do. I don't know the type parameter T in advance (as in `Task`) so I need a fair amount of esoteric reflection code to call a strongly typed method like that. – Alberto Chiesa Mar 03 '17 at 15:05

2 Answers2

4

The easiest way to make it work syntactically would be Task.FromResult. Now, that doesn't make your method asyncronous. But there is no extra boxing or unboxing involved, at least not more than before.

public static Func<ServiceType, PayloadType, Task> BuildServiceMethodInvocation<ServiceType>(MethodInfo serviceMethod)
{
    return (service, payload) =>
    {
        try
        {
            // payload is mapped onto an object[]
            var parameters = ReadPayload(payload)
            var result = serviceMethod.Invoke(service, parameters);

            // forward the task if it already *is* a task
            var task = (result as Task) ?? Task.FromResult(result);

            return task;
        }
        catch (TargetInvocationException e)
        {
            // bla bla bla handle the error
            throw;
        }
    };
}
nvoigt
  • 75,013
  • 26
  • 93
  • 142
  • I don't think this will wrap handle correctly the case of result of type Task. Please see the Edit. – Alberto Chiesa Mar 03 '17 at 13:44
  • @A.Chiesa I'm afraid I don't understand. Your `Invoke` returns an object. `FromResult` takes an object. What do you think could be handled incorrectly? – nvoigt Mar 03 '17 at 13:47
  • @A.Chiesa I have included a line to use the task that exists, if your Invoke already returns a Task. – nvoigt Mar 03 '17 at 13:51
  • Did you read my edit? If result is a `Task` I would like to get a `Task` returning the int value, not a wrapped task. – Alberto Chiesa Mar 03 '17 at 13:51
  • Maybe I'm wrong, but if I return a `Task` instead of a `Task` how do I read the result of the task, to be later serialized? Isn't `Task` like a void-returning method? – Alberto Chiesa Mar 03 '17 at 14:00
1

Ok, putting together the hints by @Evk (and John Skeet, and Marc Gravell) I came up with a solution that seems to work.

The invoke was changed in this way:

public static Func<ServiceType, PayloadType, object> BuildServiceMethodInvocation<ServiceType>(MethodInfo serviceMethod)
{
    var taskWrappingFunc = BuildTaskWrapperFunction(serviceMethod.ReturnType);

    return (service, payload) =>
    {
        try
        {
            // payload is mapped onto an object[]
            var parameters = ReadPayload(payload)
            var result = serviceMethod.Invoke(service, parameters);

            return taskWrappingFunc(result);
        }
        catch (TargetInvocationException e)
        {
            // bla bla bla handle the error
            throw;
        }
    };
}

The BuildTaskWrapperFunction is responsible for building a function that will take an object and will return a Task<object>, actually extracting and reboxing a result as needed.

    public static Func<object, Task<object>> BuildTaskWrapperFunction(Type returnType)
    {
        // manage Task<T> types
        Type taskResultType;
        if (IsATaskOfT(returnType, out taskResultType))
        {
            var f = MakeTaskWrappingMethodInfo.MakeGenericMethod(returnType);
            return (Func<object, Task<object>>)f.Invoke(null, new object[0]);
        } 

        // Manage Task
        if (typeof(Task).IsAssignableFrom(returnType)) return WrapBaseTask;

        // everything else: just wrap the synchronous result.
        return obj => Task.FromResult(obj);
    }

    // A Task is waited and then null is returned.
    // questionable, but it's ok for my scenario.
    private static async Task<object> WrapBaseTask(object obj)
    {
        var task = (Task) obj;
        if (task == null) throw new InvalidOperationException("The returned Task instance cannot be null.");
        await task;
        return null;
    }

    /// <summary> This method just returns a func that awaits for the typed task to complete
    /// and returns the result as a boxed object. </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static Func<object, Task<object>> WrapTypedTask<T>()
    {
        return async obj => await (Task<T>)obj;
    }

    private static readonly Type TypeOfTask = typeof(Task<>);

    /// <summary> Returns true if the provided type is a Task&lt;T&gt; or
    /// extends it. </summary>
    /// <param name="type"></param>
    /// <param name="taskResultType">The type of the result of the Task.</param>
    /// <returns></returns>
    public static bool IsATaskOfT(Type type, out Type taskResultType)
    {
        while (type != null)
        {
            if (type.IsGenericType && type.GetGenericTypeDefinition() == TypeOfTask)
            {
                taskResultType = type.GetGenericArguments()[0];
                return true;
            }

            type = type.BaseType;
        }

        taskResultType = null;
        return false;
    }
Alberto Chiesa
  • 7,022
  • 2
  • 26
  • 53