8

I'm trying to write code that will infer types from a parameter list and then call the method that matches those parameters. This works very well, except when the parameter list has a null value in it.

I am wondering how I might cause the Type.GetMethod call to match a function/overload, even with a null parameter in the parameters list.

object CallMethodReflection(object o, string nameMethod, params object[] args)
{
    try
    {
        var types = TypesFromObjects(args);
        var theMethod = o.GetType().GetMethod(nameMethod, types);
        return (theMethod == null) ? null : theMethod.Invoke(o, args);
    }
    catch (Exception ex)
    {
        return null;
    }
}
Type[] TypesFromObjects(params object[] pParams)
{
    var types = new List<Type>();
    foreach (var param in pParams)
    {
        types.Add((param == null) ? null : param.GetType());
    }
    return types.ToArray();
}

The main problem line is the types.Add((param == null) ? null : param.GetType());, which will cause the GetMethod call to fail with a null value in the types array.

void Function1(string arg1){ }
void Function1(string arg1, string arg2){ }
void Function1(string arg1, string arg2, string arg3){ }
void Function2(string arg1){ }
void Function2(string arg1, int arg2){ }
void Function2(string arg1, string arg2){ }

/*1*/ CallMethodReflection(obj, "Function1", "String", "String"); // This works
/*2*/ CallMethodReflection(obj, "Function1", "String", null); // This doesn't work, but still only matches one overload
/*3*/ CallMethodReflection(obj, "Function2", "String", "String"); // This works
/*4*/ CallMethodReflection(obj, "Function2", "String", null); // This doesn't work, and I can see why this would cause problems

Mainly, I'm trying to determine how to change my code so that line /*2*/ works as well.

palswim
  • 11,856
  • 6
  • 53
  • 77
  • Are you using .NET 4? .NET 4 already supports dynamic keyword which lets you call methods and method resolution happens at runtime. – Akash Kava May 23 '11 at 08:28

8 Answers8

6

There are overrides to the GetMethod call which take an object derived from the Binder class. This allows you to override the default method binding and return the method you want to use, based on the actual parameters passed. This is essentially what the two other answers are doing as well. There is some sample code here:

http://msdn.microsoft.com/en-us/library/system.reflection.binder.aspx

ScottTx
  • 1,483
  • 8
  • 12
5

An option that has not been mentioned is to use Fasterflect, a library designed to make reflection tasks easier and faster (through IL generation).

To invoke a method given a dictionary of named parameters (or an object with properties that should be used as parameters), you can invoke the best match like this:

obj.TryCallMethod( "SomeMethod", argsDictionary );
obj.TryCallMethod( "AnotherMethod", new { Foo = "Bar" } );

If all you have are the parameter values and their ordering, you can use another overload:

obj.TryCallMethodWithValues( "MyMethod", 42, "foo", "bar", null, 2.0 );

PS: You'll need to obtain the latest bits from source control to take advantage of the TryCallMethodWithValues extension.

Disclaimer: I am a contributor to the Fasterflect project.

Morten Mertner
  • 9,414
  • 4
  • 39
  • 56
3

For any parameter that is null you could just match to any reference type. The following very simple/naive code will work for your methods as shown, but it doesn't handle things like exceptions on ambiguities or more complex cases using ref/out parameters or being able to pass a derived type to the method or generic methods.

If you are using 4.0 then simply using dynamic might be a better choice.

object CallMethodReflection(object o, string nameMethod, params object[] args)
{
    try
    {
        var types = TypesFromObjects(args);
        var oType = o.GetType();
        MethodInfo theMethod = null;

        // If any types are null have to perform custom resolution logic
        if (types.Any(type => type == null)) 
        {
            foreach (var method in oType.GetMethods().Where(method => method.Name == nameMethod))
            {
                var parameters = method.GetParameters();

                if (parameters.Length != types.Length)
                    continue;

                //check to see if all the parameters match close enough to use
                bool methodMatches = true;
                for (int paramIndex = 0; paramIndex < parameters.Length; paramIndex++)
                {
                    //if arg is null, then match on any non value type
                    if (args[paramIndex] == null)
                    {
                        if (parameters[paramIndex].ParameterType.IsValueType)
                        {
                            methodMatches = false;
                            break;
                        }
                    }
                    else //otherwise match on exact type, !!! this wont handle things passing a type derived from the parameter type !!!
                    {
                        if (parameters[paramIndex].ParameterType != args[paramIndex].GetType())
                        {
                            methodMatches = false;
                            break;
                        }
                    }
                }

                if (methodMatches)
                {
                    theMethod = method;
                    break;
                }
            }
        }
        else
        {
            theMethod = oType.GetMethod(nameMethod, types);
        }

        Console.WriteLine("Calling {0}", theMethod);
        return theMethod.Invoke(o, args);
    }
    catch (Exception ex)
    {
        Console.WriteLine("Could not call method: {0}, error: {1}", nameMethod, ex.ToString());
        return null;
    }
}
BrandonAGr
  • 5,827
  • 5
  • 47
  • 72
2

I think you would have to do:

var methods = o.GetType().GetMethods().Where(m => m.Name == methodName);

Then essentially do your own overload resolution. You could try your existing method first, catch the exception and then try the above.

Ben Robinson
  • 21,601
  • 5
  • 62
  • 79
  • Agreed. Since you get an `object` with the "value" of `null`, which basically isn't an object at all and it doesn't make sense of talking about "the type of nothing".. An alternative would be, as Ben says, to do your own mapping if the one you have fails, and check if you find a method that has an argument type at the specific position that is nullable. – Robin May 24 '11 at 10:44
1

Thanks to the MSDN link as well as some additional SO discussion and an outside forum discussion involving a prominent SO member, I have tried to implement my own solution, which is working for me so far.

I created a class which inherited the Binder class and put my logic to handle the potentially null arguments/types in there.

object CallMethodReflection(object o, string nameMethod, params object[] args)
{
    try
    {
        var types = TypesFromObjects(args);
        var theMethod = o.GetType().GetMethod(nameMethod, CustomBinder.Flags, new CustomBinder(), types, null);
        return (theMethod == null) ? null : theMethod.Invoke(o, args);
    }
    catch (Exception ex)
    {
        return null;
    }
}
Type[] TypesFromObjects(params object[] pParams)
{
    var types = new List<Type>();
    foreach (var param in pParams)
    {
        types.Add((param == null) ? typeof(void) : param.GetType()); // GetMethod above doesn't like a simply null value for the type
    }
    return types.ToArray();
}
private class CustomBinder : Binder
{
    public const BindingFlags Flags = BindingFlags.Public | BindingFlags.Instance;
    public override MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] matches, Type[] types, ParameterModifier[] modifiers)
    {
        if (matches == null)
            throw new ArgumentNullException("matches");
        foreach (var match in matches)
        {
            if (MethodMatches(match.GetParameters(), types, modifiers))
                return match;
        }
        return Type.DefaultBinder.SelectMethod(bindingAttr, matches, types, modifiers); // No matches. Fall back to default
    }
    private static bool MethodMatches(ParameterInfo[] parameters, Type[] types, ParameterModifier[] modifiers)
    {
        if (types.Length != parameters.Length)
            return false;
        for (int i = types.Length - 1; i >= 0; i--)
        {
            if ((types[i] == null) || (types[i] == typeof(void)))
            {
                if (parameters[i].ParameterType.IsValueType)
                    return false; // We don't want to chance it with a wonky value
            }
            else if (!parameters[i].ParameterType.IsAssignableFrom(types[i]))
            {
                return false; // If any parameter doesn't match, then the method doesn't match
            }
        }
        return true;
    }
}

Since the Binder class is an abstract class, you have to override a few other members to actually use this code, but most of my overrides just front the Type.DefaultBinder object.

public override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] matches, object value, CultureInfo culture)
{
    return Type.DefaultBinder.BindToField(bindingAttr, matches, value, culture);
}
Community
  • 1
  • 1
palswim
  • 11,856
  • 6
  • 53
  • 77
0

I didn't test it and i think the other answers are much better, but i'm wondering why this wouldn't work:

foreach (var param in pParams.Where(p => p != null)
{
    types.Add(param.GetType());
}
Oliver
  • 43,366
  • 8
  • 94
  • 151
  • Then I'd be missing some types and could miss a match because I have a type list of 3 when I need to match a function that takes 4 arguments. – palswim May 24 '11 at 15:41
0

You could approach the problem by implementing your own GetMethod that iterates through all the method in the object and determine which one is the best match, I hope this helps. I tested the following method with the example you provided and it worked

MethodInfo SmarterGetMethod(object o, string nameMethod, params object[] args)
{
var methods = o.GetType().GetMethods();
var min = args.Length;
var values = new int[methods.Length];
values.Initialize();
//Iterates through all methods in o
for (var i = 0; i < methods.Length; i += 1)
{
    if (methods[i].Name == nameMethod)
    {
        var parameters = methods[i].GetParameters();
        if (parameters.Length == min)
        {
            //Iterates through parameters
            for (var j = 0; j < min; j += 1)
            {
                if (args[j] == null)
                {
                    if (parameters[j].ParameterType.IsValueType)
                    {
                        values[i] = 0;
                        break;
                    }
                    else
                    {
                        values[i] += 1;
                    }
                }
                else
                {
                    if (parameters[j].ParameterType != args[j].GetType())
                    {
                        values[i] = 0;
                        break;
                    }
                    else
                    {
                        values[i] += 2;
                    }
                }
            }
            if (values[i] == min * 2) //Exact match                    
                return methods[i];
        }
    }
}

var best = values.Max();
if (best < min) //There is no match
    return null;
//Iterates through value until it finds first best match
for (var i = 0; i < values.Length; i += 1)
{
    if (values[i] == best)
        return methods[i];
}
return null; //Should never happen
}
0
  1. If none of parameters is NULL you perform usual method call, if one is null however
  2. else if at least one is null you take different approach:
  3. build parameter type list from parameters : like "int, char, null, int"
  4. get functions overloads with same number of parameters for your function name
  5. see whether there is just one matching function, cause if there are 2 you cannot determine which to call (hardest part but fairly straightforward I think)
  6. call the function you figured out with your parameters and nulls
Valentin Kuzub
  • 11,703
  • 7
  • 56
  • 93