DuckQueen
DuckQueen

Reputation: 802

How to call a function with multiple in\out params not knowing their order using Reflection in C#?

I have a type instance t:

and two Dictionaries In and Out

I assume that there are no other arguments required in order to call an Action of t.

So how to call a function with multiple inputs and outputs using Reflection/Dynamitey knowing their types yet not their order?

Upvotes: 0

Views: 630

Answers (1)

pinkfloydx33
pinkfloydx33

Reputation: 12739

When you invoke a method via reflection, you pass an object[] containing all of the parameters. For out parameters, you leave the corresponding value in the array as null. Once the method has completed you can retrieve the value from the corresponding position in the array.

To demonstrate this, let's assume the following two classes:

public class A
{
    public string Something { get; set; }
}

public class B
{
    public int Id { get; set; }
    public override string ToString() => "[B] Id=" + Id; // for Console.WriteLine
}

We are going to invoke the following static method which includes three input parameters and one out parameter (of type B):

public static void MyMethod(int i, string x, A a, out B b)
{
    Console.WriteLine("In Method - i={0}", i);
    Console.WriteLine("In Method - x={0}", x);
    Console.WriteLine("In Method - A.Something={0}", a.Something);

    b = new B { Id = 33 };
}

To invoke the method with the appropriate arguments we will loop over the array returned by GetParameters and populate our object[].

To determine if our parameter is an out parameter we must look at two things: ParameterInfo.IsOut and ParameterInfo.ParameterType.IsByRef*. If both values are true we will simply leave a space in the array. Otherwise we will use the ParameterType property to query the dictionary for the appropriate instance (throwing an exception if we don't have an instance):

var dict = new Dictionary<Type, object>
{
    [typeof(int)] = 5,
    [typeof(string)] = "Hello",
    [typeof(A)] = new A { Something = "World" }
};

MethodInfo method = typeof(Program).GetMethod(nameof(MyMethod));

// get the parameters and create the input array
var @params = method.GetParameters();
var methodArgs = new object[@params.Length];
// loop over the parameters
// see below for LINQ'ified version
for (var i = 0; i < @params.Length; i++)
{
    var p = @params[i];
    // if it's an output parameter, ignore its value
    if (p.IsOut && p.ParameterType.IsByRef)
        continue;
    // get the value based on the parameter type or throw if not found
    if (!dict.TryGetValue(p.ParameterType, out var arg))
        throw new InvalidOperationException("Cannot find parameter of type " + p.ParameterType.Name);

    methodArgs[i] = arg;
}

Finally we will invoke the method using the arguments array we have created. Once the method completes, we can access the output parameter via the appropriate value in the array

// Note: if Method is NOT a static method, we need to pass the instance as
// the first parameter. This demo uses a static method so we pass null
method.Invoke(null, methodArgs);
Console.WriteLine("In Main - B={0}", methodArgs[3]);

Running the above will print the following:

In Method - i=5

In Method - x=Hello

In Method - A.Something=World

In Main - B=[B] Id=33

You can "shorten" the loop to the following:

var methodArgs2 = method.GetParameters()
    .Select(param =>
        param.IsOut && param.ParameterType.IsByRef ? null :
        dict.TryGetValue(param.ParameterType, out var pValue) ? pValue :
        throw new InvalidOperationException($"Parameter of type {param.ParameterType.Name} was not found"))
    .ToArray();

If you are absolutely certain that your dictionary will contain all of the appropriate types, we can make it simpler:

var methodArgs = method.GetParameters().Select(p => p.IsOut && p.ParameterType.IsByRef ? null : dict[p.ParameterType]).ToArray();

Note: if you statically know the type you can cast it back from object.

B b = (B)methodArgs[3];

Unfortunately you will not be able to do this with the Type object alone. If you don't know the type you could switch using pattern matching or is/as

Note: you could also get the parameter's zero-based index (something you mention above) using the ParameterInfo.Position property:

var index = p.Position;
methodArgs[index] = ... ;

* Testing only ParameterInfo.IsOut is not sufficient. Compilers may insert the OutAttribute in other cases (such as COM). An out parameter will have both this attribute (which is what sets IsOut) and will also have a by-ref type T&. Similarly, ref parameters will have the IsByRef flag, but they will not have IsOut. See this answer for more details.

Now, if you had ref parameters you'd have to do things a bit differently. First of all ref typically assumes that a value exists as a pre-condition, but VB doesn't necessarily require it (where out does not exist). To check for parameters of this type you'd first have to check !p.IsOut && p.ParameterType.IsByRef. Then, to get the instance from your dictionary you'll need to turn the ByRef type (T&) into a normal one (T) using GetElementType:

if (!dict.TryGetValue(p.ParameterType.GetElementType(), out var pValue)) {
   // if you know it's safe to pass null, then continue 
   // otherwise you may need to create a new object using Activator
} 

Upvotes: 3

Related Questions