4

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.)

Bob Sammers
  • 3,080
  • 27
  • 33
  • may be this helps :https://stackoverflow.com/questions/232535/how-do-i-use-reflection-to-call-a-generic-method – Ehsan Sajjad Oct 11 '17 at 14:57
  • @EhsanSajjad Unfortunately, the answers to that questions (and other similar questions) assume that the method can be found by just using the name - I need to include the parameters and that is where I'm having trouble. – Bob Sammers Oct 11 '17 at 15:07
  • Instead of calling `GetMethod` with type parameters, why not call `GetMethods` then iterate over the results (matching on name) and compare parameter types there? – BurnsBA Oct 11 '17 at 16:17
  • I will say, it seems odd to _require_ that classes implement certain static methods. Wouldn't that be flushed out at compile-time? Or are you calling these static methods through reflection? – D Stanley Oct 11 '17 at 18:23
  • @DStanley Functionality from an obsoleted library class is being replicated in a newer one (which, unlike the original, is static). I want to ensure in a test that all the old members are covered and ongoing maintenance during the changeover period is replicated between them. They are mostly facades so the signature is the important part. – Bob Sammers Oct 11 '17 at 18:49

3 Answers3

1

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)  
BurnsBA
  • 4,347
  • 27
  • 39
  • According to the MSDN page for `Type.GetGenericTypeDefinition()`, `Type.IsGenericType` indicates whether calling this method is appropriate. – Bob Sammers Oct 12 '17 at 12:54
  • I may have been a bit hasty accepting this answer. Although the demonstration using `typeof(Action<>)` now shows eqaulity, `GetMethod()` apparently still cannot match on supplied parameters that have been created using `GetGenericTypeDefinition()`. (contd.) – Bob Sammers Oct 12 '17 at 12:55
  • To fix this, I tried creating a new `GetMethod()` extension that uses `GetMethods()` and then iterates through the returned methods looking for matches on name and parameters - I can also then use `GetGenericTypeDefinition()` on these too. This has partially worked, but is still not correctly matching normal generics (of the form `void SomeMethod(T parameter)`). I'm wondering if it is to do with comparing an instance method to a generic one. – Bob Sammers Oct 12 '17 at 12:55
  • Ok, so I think an actual answer is going to be rather different from what I posted and is going to involve writing some custom methods as it appears that there are some fundamental issues with the builtin reflection support for generics. I would point you to https://stackoverflow.com/a/7182379/1462295 which actually looks like it might address most of your issues. – BurnsBA Oct 12 '17 at 13:40
  • @BobSammers see edit (feel free to post your own answer and accept it, this wasn't as helpful as I thought it would be) – BurnsBA Oct 12 '17 at 13:57
1

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.

Bob Sammers
  • 3,080
  • 27
  • 33
0

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){}
Rene Niediek
  • 147
  • 9