Bob Sammers
Bob Sammers

Reputation: 3270

Type.GetMethod() for generic method

For testing purposes, I'm checking to see if a series of method signatures in a reference class have been implemented on a different static class. For most of them the following is working:

private static IEnumerable<Signature> GetMethodSigs(Type type)
{
    // Get MethodInfos, filter and project into signatures
    var methods = type.GetMethods(
                          BindingFlags.Public
                        | BindingFlags.DeclaredOnly
                        | BindingFlags.Static 
                        | BindingFlags.Instance)
                    .Where(mi => !mi.Name.StartsWith("get_"))
                    .Where(mi => !mi.Name.StartsWith("set_"))
                    .Select(o => new Signature(o.Name, o.ReturnType, o.GetParameters().Select(pi => pi.ParameterType)));

    return methods;
}

private static MethodInfo FindMethod(Type type, Signature sig)
{
    MethodInfo member = type.GetMethod(
                                sig.Name,
                                BindingFlags.Public | BindingFlags.Static,
                                null,
                                sig.ParameterTypes.ToArray(),
                                null);
    return member;
}

public struct Signature
{
    public string Name;
    public Type ReturnType;
    public IEnumerable<Type> ParameterTypes;

    public Signature(string name, Type returnType, IEnumerable<Type> parameterTypes = null)
    {
        Name = name;
        ReturnType = returnType;
        ParameterTypes = parameterTypes;
    }
}

This is part of a test class, but imagine the following code driving the process:

foreach(var sig in GetMethodSigs(typeof(ReferenceClass)))
    Console.WriteLine(FindMethod(typeof(TestClass), sig)?.ToString());

The following method signature is being picked up okay on ReferenceClass, but FindMethod() is not finding an equivalent method (which does exist!) on TestClass:

public static void SomeMethod<T>(SomeDelegate<T> del)

GetMethods() gives me a type for the del parameter (SomeDelegate`1), but this is apparently not a suitable type to search on in GetMethod(), as FindMethod() returns null for this input.

Any idea how I can manipulate the values returned from GetMethods() to search for a generic method using GetMethod() (with the obvious subtlety that the generic parameter is used in a delegate, rather than a simple parameter type)?

(I realise there is a version of GetMethod() just taking a name, but as some of the method names are overloaded, I need to search for the parameter types as well.)

Upvotes: 4

Views: 7329

Answers (3)

Bob Sammers
Bob Sammers

Reputation: 3270

Instead of one of the Type.GetMethod() overloads, I ended up using GetMethods() on the target class and looping through all the members using an extension method to compare each parameter type (note C#7 only local function):

public static bool Similar(this Type reference, Type type)
{
    if (reference.IsGenericParameter && type.IsGenericParameter)
    {
        return reference.GenericParameterPosition == type.GenericParameterPosition;
    }

    return ComparableType(reference) == ComparableType(type);

    Type ComparableType(Type cType)
        => cType.IsGenericType ? cType.GetGenericTypeDefinition() : cType;
}

This considers two types to be "similar" if:

  • They are simple types and compare equal using the == operator
  • They are generic types and are of the type with the same index in the list of generic arguments (i.e. in SomeMethod<T,S>(S parameter), the one and only parameter type would be considered equal to that in SomeMethod<T1,T2>(T2 parm) but not SomeMethod<T,S>(T parameter)).
  • They are identical types with a nested generic parameter. In this case, the fact that it is a generic type is noted, but nothing about the parameter is probed further (so the parameter type on SomeMethod<T,S>(Action<T> parameter) would be "similar" to SomeMethod<T,S>(Action<S> parameter).

This is not ideal, but it turns out to be a surprisingly hard problem! It worked for my use case where it covered all my cases and because of the nature of the project (analysing legacy code) no new cases are likely to arise.

Similar() is used in the following extension on Type, which is intended to replace Type.GetMethod() and implements the loop I mentioned above:

public static MethodInfo GetMethodWithGenerics(
                            this Type type,
                            string name, Type[] parameters,
                            BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
{
    var methods = type.GetMethods(flags);

    foreach (var method in methods)
    {
        var parmeterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();

        if (method.Name == name && parmeterTypes.Count() == parameters.Length)
        {
            bool match = true;

            for (int i = 0; i < parameters.Length; i++)
                match &= parmeterTypes[i].Similar(parameters[i]);

            if (match)
                return method;
        }
    }

    return null;
}

As BurnsBA said below his answer, there seem to be some fundamental issues with the built-in reflection support for generics and there doesn't seem to be a simple solution to my original problem. I arrived at this answer after consideration of BurnBA's answer here and to the one he linked to on another question. This answer will be especially useful to anyone who wishes to produce a more thorough version of this comparison.

Anyone finding this useful should probably consider upvoting either or both of these.

Upvotes: 1

BurnsBA
BurnsBA

Reputation: 4929

I didn't notice for quite some time that you are comparing two different class definitions:

foreach(var sig in GetMethodSigs(typeof(ReferenceClass)))
    Console.WriteLine(FindMethod(typeof(TestClass), sig)?.ToString());

This is notable because if the classes were the same, your above code would work. The issue is the way that generics are handled (briefly looking at Type source, it appears Equals uses a reference comparison, but == is offloaded to the compiler I think. The point is, the different generic methods types are not reference equivalent. But verifying what actually happens is left as an exercise to the reader).

A quick demonstration (you can run this in the interactive shell)

public class D { public static void Method<T>(Action<T> t) { } }
(new D()).GetType().GetMethod("Method").GetParameters()[0].ParameterType == (typeof(Action<>))

output:

false  

However, if you check the Types, both look the same:

> (new D()).GetType().GetMethod("Method").GetParameters()[0].ParameterType.ToString()
"System.Action`1[T]"
> (typeof(Action<>)).ToString()
"System.Action`1[T]"  

Ok, that's it for the problem demonstration. You should be able to work around this issue by changing your select statement in GetMethodSigs to call GetGenericTypeDefinition() on each parameter type:

.Select(o => new Signature(o.Name, o.ReturnType, o.GetParameters().Select(pi => pi.ParameterType.GetGenericTypeDefinition())));  

You can see the following now shows equality:

> (new D()).GetType().GetMethod("Method").GetParameters()[0].ParameterType.GetGenericTypeDefinition() == (typeof(Action<>)).GetGenericTypeDefinition()
true   

Of course, this is only for generic types. You'll have to add in some logic to only call .GetGenericTypeDefinition() as appropriate in the above select statement.

More from https://stackoverflow.com/a/1855248/1462295


Edit:

Using the code from https://stackoverflow.com/a/7182379/1462295

(Note the empty class T from above)

public class D
{
    public static void Method<TT>(Action<TT> t) { }
    public static void Method<TT>(TT t) { }
}

public class RefD
{
    public static void Method<TT>(Action<TT> t) { }
    public static void Method<TT>(TT t) { }
}

foreach(var sig in GetMethodSigs(typeof(RefD)))
    Console.WriteLine(GetMethodExt(typeof(D), sig.Name, sig.ParameterTypes.ToArray())?.ToString());

output

Void Method[TT](System.Action`1[TT])
Void Method[TT](TT)  

Upvotes: 1

Rene Niediek
Rene Niediek

Reputation: 147

May you can try something like

string name = "MyFunction";
Type type = typeof(Program);

MethodInfo member = type.GetMethods(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Instance)
.Single(m => m.Name == name && m.GetParameters().Select(p => p.ParameterType.Name).SequenceEqual(new [] { typeof(MyClass<>).Name }));

public void MyFunction<T>(MyClass<T> test){}

Upvotes: 0

Related Questions