Search code examples
c#reflectionsystem.reflectiondynamicobject

C# Dynamic Object call TryInvoke with an unknown array of arguments


I need to invoke a given DynamicObject with a given array of arguments. However, I am struggling on how exactly to do this.

What I would like to do:

using System.Dynamic;
using System.Reflection;

InvokeDynamicObject(new A(), ((object[])["asd", 1, null]));

object? InvokeDynamicObject(DynamicObject dyn, object?[] args)
{
    dyn.TryInvoke(/* what? */, args, out object? res);
    return res;
}

class A : DynamicObject
{
    public override bool TryInvoke(InvokeBinder binder, object?[]? args, out object? result)
    {
        Console.WriteLine($"Called with args {String.Join(", ", (args ?? []).Select(a => a is null ? "null" : a.GetType().Name))}");
        result = null;
        return true;
    }
}

If the arguments were fixed it would be easy ((dynamic)dyn)(arg1, arg2, arg3) but how do I do this for an array of provided args?

I think I need to call TryInvoke using reflection, but what do I provide for the InvokeBinder argument? I didn't find any way to create one, do I need to implement one myself (and what would that need to do?)?


Solution

  • First, you should understand that you could never exactly replicate what a compiler would generate for the same list of arguments. The compiler has access to compile-time type information, which could affect the binding process. With a object?[], you don't have access to that. F("foo") and F((object)"foo") might bind to totally different methods, but your InvokeDynamicObject method cannot distinguish between them. Your method doesn't know the compile time type of the arguments.

    Second, I encourage you to check out on SharpLab, how a dynamically bound call is done under the hood.

    1. The compiler creates an array of CSharpArgumentInfos. One for each argument, and an extra one for the object on which you are calling the method.
    2. A CallSiteBinder is created by calling Microsoft.CSharp.RuntimeBinder.Binder.Invoke. This method takes some flags, the type in which this call occurs (for access control purposes), and the argument infos created in step 1
    3. The CallSiteBinder is used to create a CallSite<T>, where T is a delegate type. This will be a different delegate type depending on the types and number of arguments. For example, for the argument list (1,"foo",true), T would be Func<CallSite, object, int, string, bool, object>. The first object is the type of the receiver, and the second object is the return type.
    4. The CallSite is stored in a static field so that it can be reused.
    5. CallSite.Target is invoked with the corresponding parameters.

    You'd need to do each of these steps dynamically. The most difficult one is probably 3, since you need to find an appropriate delegate type. Func only goes up to 16 parameters, so for calls with more than 14 arguments, you need to load your own delegate types dynamically!

    That aside, here is a basic outline. Be aware that I've used some !s here.

    static object? InvokeDynamicObject(DynamicObject dyn, object?[] args)
    {
        // step 1
        var argInfos = Enumerable.Repeat(CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), args.Length + 1).ToArray();
    
        // here I have used the runtime type of the arguments, and typeof(object) if the argument is null
        var argTypes = args.Select(x => x?.GetType() ?? typeof(object)).ToArray();
    
        // this creates the T in CallSite<T>
        var callSiteDelegate = Type.GetType($"System.Func`{args.Length + 3}")!.MakeGenericType([typeof(CallSite), typeof(object), ..argTypes, typeof(object)]);
    
        // steps 2 and 3, assuming this happens in a class called 'Program'
        var callsite = CallSite.Create(callSiteDelegate, Binder.Invoke(CSharpBinderFlags.None, typeof(Program), argInfos));
    
        // step 5
        var field = typeof(CallSite<>).MakeGenericType(callSiteDelegate).GetField("Target");
        return ((Delegate)field!.GetValue(callsite)!).DynamicInvoke([callsite, dyn, ..args]);
    }
    

    This doesn't handle calls of more than 14 arguments, and it doesn't cache the CallSite. You'd need to dynamically create new delegate types to support more than 14 arguments, possibly using things from System.Reflection.Emit. You might want to store the CallSites in a Dictionary<int, CallSite>, one for each arity.