54

I'm trying to retrieve MethodInfo for Where method of Enumerable type:

typeof (Enumerable).GetMethod("Where", new Type[] { 
     typeof(IEnumerable<>), 
     typeof(Func<,>) 
})

but get null. What am I doing wrong?

Noctis
  • 11,507
  • 3
  • 43
  • 82
SiberianGuy
  • 24,674
  • 56
  • 152
  • 266
  • 1
    Getting the type of Func seems the obstacle to me. – Hans Passant Oct 27 '10 at 18:02
  • Another exact duplicate: [select-right-generic-method-with-reflection](http://stackoverflow.com/questions/3631547/select-right-generic-method-with-reflection) – nawfal Jan 18 '14 at 05:55

2 Answers2

55

That previous answer works for some cases, however:

  • It doesn't handle nested generic types, such as a parameter type of Action<IEnumerable<T>>. It will treat all Action<> as matches, for example, string.Concat(IEnumerable<string>) and string.Concat<T>(IEnumerable<T>) will both match if searching for "Concat" with type IEnumerable<> on the string type. What is really desirable is handling nested generic types recursively, while treating all generic parameters as matching each other regardless of name while NOT matching concrete types.
  • It returns the first method matched rather than throwing an exception if the result is ambiguous, like type.GetMethod() does. So, you might get the method you wanted if you're lucky, or you might not.
  • Sometimes it will be necessary to specify BindingFlags in order to avoid ambiguity, such as when a derived class method 'hides' a base class method. You normally want to find base class methods, but not in a specialized case where you know the method you're looking for is in the derived class. Or, you might know you're looking for a static vs instance method, public vs private, etc. and don't want to match if it's not exact.
  • It doesn't address another major fault with type.GetMethods(), in that it also doesn't search base interfaces for methods when looking for a method on an interface type. OK, maybe that's being picky, but it's another major flaw in GetMethods() that has been a problem for me.
  • Calling type.GetMethods() is inefficient, type.GetMember(name, MemberTypes.Method, ...) will return only methods with a matching name instead of ALL methods in the type.
  • As a final nit-pick, the name GetGenericMethod() could be misleading, since you might be trying to find a non-generic method that happens to have a type parameter somewhere in a parameter type due to a generic declaring type.

Here's a version that addresses all of those things, and can be used as a general-purpose replacement for the flawed GetMethod(). Note that two extension methods are provided, one with BindingFlags and one without (for convenience).

/// <summary>
/// Search for a method by name and parameter types.  
/// Unlike GetMethod(), does 'loose' matching on generic
/// parameter types, and searches base interfaces.
/// </summary>
/// <exception cref="AmbiguousMatchException"/>
public static MethodInfo GetMethodExt(  this Type thisType, 
                                        string name, 
                                        params Type[] parameterTypes)
{
    return GetMethodExt(thisType, 
                        name, 
                        BindingFlags.Instance 
                        | BindingFlags.Static 
                        | BindingFlags.Public 
                        | BindingFlags.NonPublic
                        | BindingFlags.FlattenHierarchy, 
                        parameterTypes);
}

/// <summary>
/// Search for a method by name, parameter types, and binding flags.  
/// Unlike GetMethod(), does 'loose' matching on generic
/// parameter types, and searches base interfaces.
/// </summary>
/// <exception cref="AmbiguousMatchException"/>
public static MethodInfo GetMethodExt(  this Type thisType, 
                                        string name, 
                                        BindingFlags bindingFlags, 
                                        params Type[] parameterTypes)
{
    MethodInfo matchingMethod = null;

    // Check all methods with the specified name, including in base classes
    GetMethodExt(ref matchingMethod, thisType, name, bindingFlags, parameterTypes);

    // If we're searching an interface, we have to manually search base interfaces
    if (matchingMethod == null && thisType.IsInterface)
    {
        foreach (Type interfaceType in thisType.GetInterfaces())
            GetMethodExt(ref matchingMethod, 
                         interfaceType, 
                         name, 
                         bindingFlags, 
                         parameterTypes);
    }

    return matchingMethod;
}

private static void GetMethodExt(   ref MethodInfo matchingMethod, 
                                    Type type, 
                                    string name, 
                                    BindingFlags bindingFlags, 
                                    params Type[] parameterTypes)
{
    // Check all methods with the specified name, including in base classes
    foreach (MethodInfo methodInfo in type.GetMember(name, 
                                                     MemberTypes.Method, 
                                                     bindingFlags))
    {
        // Check that the parameter counts and types match, 
        // with 'loose' matching on generic parameters
        ParameterInfo[] parameterInfos = methodInfo.GetParameters();
        if (parameterInfos.Length == parameterTypes.Length)
        {
            int i = 0;
            for (; i < parameterInfos.Length; ++i)
            {
                if (!parameterInfos[i].ParameterType
                                      .IsSimilarType(parameterTypes[i]))
                    break;
            }
            if (i == parameterInfos.Length)
            {
                if (matchingMethod == null)
                    matchingMethod = methodInfo;
                else
                    throw new AmbiguousMatchException(
                           "More than one matching method found!");
            }
        }
    }
}

/// <summary>
/// Special type used to match any generic parameter type in GetMethodExt().
/// </summary>
public class T
{ }

/// <summary>
/// Determines if the two types are either identical, or are both generic 
/// parameters or generic types with generic parameters in the same
///  locations (generic parameters match any other generic paramter,
/// but NOT concrete types).
/// </summary>
private static bool IsSimilarType(this Type thisType, Type type)
{
    // Ignore any 'ref' types
    if (thisType.IsByRef)
        thisType = thisType.GetElementType();
    if (type.IsByRef)
        type = type.GetElementType();

    // Handle array types
    if (thisType.IsArray && type.IsArray)
        return thisType.GetElementType().IsSimilarType(type.GetElementType());

    // If the types are identical, or they're both generic parameters 
    // or the special 'T' type, treat as a match
    if (thisType == type || ((thisType.IsGenericParameter || thisType == typeof(T)) 
                         && (type.IsGenericParameter || type == typeof(T))))
        return true;

    // Handle any generic arguments
    if (thisType.IsGenericType && type.IsGenericType)
    {
        Type[] thisArguments = thisType.GetGenericArguments();
        Type[] arguments = type.GetGenericArguments();
        if (thisArguments.Length == arguments.Length)
        {
            for (int i = 0; i < thisArguments.Length; ++i)
            {
                if (!thisArguments[i].IsSimilarType(arguments[i]))
                    return false;
            }
            return true;
        }
    }

    return false;
}

Note that the IsSimilarType(Type) extension method can be made public and might be useful on its own. I know, the name isn't great - you're welcome to come up with a better one, but it might get really long to explain what it does. Also, I added yet another improvement by checking for 'ref' and array types (refs are ignored for matching, but arrays dimensions must match).

So, that's how Microsoft should have done it. It's really not that hard.

Yeah, I know, you can shorten some of that logic using Linq, but I'm not a huge fan of Linq in low-level routines like this, and also not unless the Linq is about as easy to follow as the original code, which is often NOT the case, IMO.

If you love Linq, and you must, you can replace the inner-most part of IsSimilarType() with this (turns 8 lines into 1):

if (thisArguments.Length == arguments.Length)
    return !thisArguments.Where((t, i) => !t.IsSimilarType(arguments[i])).Any();

One last thing: If you're looking for a generic method with a generic parameter, such as Method<T>(T, T[]), you'll have to find a Type which is a generic parameter (IsGenericParameter == true) to pass in for the parameter type (any one will do, because of the 'wildcard' matching). However, you can't just do new Type() - you have to find a real one (or build one with TypeBuilder). To make this easier, I added the public class T declaration, and added logic to IsSimilarType() to check for it and match any generic parameter. If you need a T[], just use T.MakeArrayType(1).

Noctis
  • 11,507
  • 3
  • 43
  • 82
Ken Beckett
  • 1,273
  • 11
  • 13
  • "So, that's how Microsoft should have done it. It's really not that hard." Yeah, except your code still doesn't work. If the argument is System.Type and the value passed in is System.RuntimeType doesn't work. – Cameron MacFarland Dec 21 '12 at 09:04
  • Cameron, I think you're probably doing something wrong to be having that problem. Maybe you called GetType() on a Type object? See the Answer on this post: http://stackoverflow.com/questions/5737840/whats-the-difference-between-system-type-and-system-runtimetype-in-c – Ken Beckett Dec 27 '12 at 03:19
  • @Ken_Beckett Nope, I just want to call a method with the signature `SomeMethod(Type t)` by going `dynamicInstance.SomeMethod(other.GetType())`. The issue is `IsSimilarType` returns false for `System.RuntimeType` and `System.Type`. Fixing that issue, I then had issues calling methods with generic parameters. All up your code does not work in all cases so I'd say the problem **is** that hard. – Cameron MacFarland Dec 27 '12 at 06:51
  • @CameronMacFarland I should also point out that your problem with RuntimeType would also be a problem with other examples here and most importantly with MS's method. Also, your misuse of my code does NOT mean that my code doesn't work, or that the problem really IS "that hard". Also, you're being rather rude to somebody who went out of his way to post some handy, useful, and free code for the public to use. If I could only revoke your individual right to use it, I would do so. – Ken Beckett Dec 29 '12 at 07:29
  • @CameronMacFarland Yes, it was obvious from your first comment that you had some sort of personal problem with my answer, and only wanted to complain rather than get help or be helpful to others. Making snarky, immature, rude, unprofessional comments is not an appropriate use of StackOverflow. It's obviously wasted on you, but for others I'll point out yet again that confusion between RuntimeType and Type has absolutely nothing to do with the hardness of this problem, nobody else's code posted here or by Microsoft handles it (because it's not necessary), and there are no bugs in my code. – Ken Beckett Dec 29 '12 at 17:51
  • I'll just leave this here... Failing Unit-Test for this answer. https://gist.github.com/4410158 – Cameron MacFarland Dec 30 '12 at 00:09
  • 2
    That got a bit heated... so to summarize this debate, the GetMethodExt() proposed here does help find methods even if they have generic arguments, unlike the existing GetMethod(). It however has the limitation that the type it's called from must match the type of the object passed in as the second argument - if the second argument is further down the inheritance tree, it won't find the method. Fair? Still seems like useful code (solves my use case), limitation noted. Everyone happy? Ken, might make sense to post this to github. – Chris Moschini Mar 21 '13 at 01:03
  • 2
    Doesn't currently find methods that have optional parameters. A solution (if the optional parameters are passed as null) is to change the IsSimilarType test to `if (!parameterInfos[i].IsOptional && !parameterInfos[i].ParameterType.IsSimilarType(parameterTypes[i]))` Additional code changes would be needed to support omitting the optional parameters. – Handcraftsman Jun 17 '13 at 22:27
  • @CameronMacFarland in the github link you posted, you're passing `typeof(object)`, instead your `SomeMethod(Type t)` method signature states that you should actually pass `typeof(Type)`. – nawfal Oct 08 '13 at 12:35
  • @KenBeckett how do you call your `GetMethodExt` if the method is generic like this: `void Pita(R r, S s, T t)`? And when you have two generic overloads `void Pita(S s, T t)` and `void Pita(T t, S s)` how do you get `MethodInfo` of first overload? In short, these things with generics is very very hard. Unless MS packs C# with some new indicator to denote generic types, its impossible. – nawfal Oct 10 '13 at 05:13
  • Can you tell me how does the previous answer (Dustin Campbell's) result in an ambiguous match (in which case it gives the first result as you say)? Simply it doesnt have to result in an ambiguous match since we're specifying the type of all parameters. – nawfal Oct 10 '13 at 05:25
  • Also, why `T.MakeArrayType(1)` instead of `typeof(T[])`? Just learning, dont take it personally. – nawfal Oct 10 '13 at 05:26
  • 1
    @nawfal Sorry, but I have far exceeded any reasonable limits of my valuable time for what started out as simply a donation of code that solves all problems related to this issue for my case (which was a full C# 5.0 compiler implementation). My generosity was rewarded not with thanks, but with being rudely hassled to make it work for other people's questionable use cases - not to mention having my answer declared irrelevant by half a dozen members declaring the question (incorrectly) as a duplicate. Now, one of those same members wants more of my time to educate him? No thank you. – Ken Beckett Oct 12 '13 at 19:42
  • @nawfal One of your 3 questions has a clear answer in my original post, and the other 2 would be trivial for you to answer for yourself. Then, if you discover something useful to others, post it. That's the way StackOverflow should be used, not as a way to avoid effort or thinking by getting others to do it for you. I've learned my lesson - there are far too many inconsiderate, disrespectful, lazy, or simply stupid people out there. They spoil everything for the rest of us. Just look at the comments section of any blog on the web, assuming you can find one that hasn't disabled comments. – Ken Beckett Oct 12 '13 at 19:54
  • 2
    @KenBeckett I see you're taking things personally far too easily. Nobody has any personal grudge against you here (**for any imaginable reason!**), and we all are grateful for the little work the members do here. That said, it doesnt give anyone the right to claim something false. Some of us are pointing to you where you have gone wrong. Rather than question our motive, kindly correct your answer using the edit feature (if you find something was wrong), or give us a reply. – nawfal Oct 12 '13 at 20:02
  • 1. No none was being rude with *questionable use cases*. Instead only pointing to you that *it's not all that easy for MS* 2. I posed some other questions based on your answer. If you're not rude, you ought to give me the benefit of the doubt that I was knowing, genuinely, not putting you down. – nawfal Oct 12 '13 at 20:03
  • 1
    3. And no, closing a question is about declaring the *question* irrelevant, not the *answer*. May be you've not been on SO much. It was a mistake to vote the question for closure which I admitted. Whats the big deal? Qtns are not deleted, and you're free to edit your answer too, and the question will still receive views. I flagged it to reopen too (btw this question is a duplicate of many other questions anyway). 4. Comments sections are for clarification and proposin improvements. If that makes it *educating*, well sir, I'm, just like any other member curios about code, dying to get educated. – nawfal Oct 12 '13 at 20:04
  • 1
    5. Kindly tell me *which one of my question* has been answered in your answer. 6. No, again if I know the answer I would post it. Ignorance is not a sin. Rudeness is. Its only because I do not know the answer I'm here. **Lastly Beckett, kindly go around posts here on SO to find that people do suggest improvements, and question questionable answers (or a part of it). That's in the game.** And yes I'm one of those guys who has up voted your answer for it being genuinely a nice answer. But I got questions, sadly and that's not laziness, its ignorance. – nawfal Oct 12 '13 at 20:12
31

Unfortunately, generics are not well-supported in .NET Reflection. In this particular case, you'll need to call GetMethods and then filter the result set for the method you're looking for. An extension method like the following should do the trick.

public static class TypeExtensions
{
    private class SimpleTypeComparer : IEqualityComparer<Type>
    {
        public bool Equals(Type x, Type y)
        {
            return x.Assembly == y.Assembly &&
                x.Namespace == y.Namespace &&
                x.Name == y.Name;
        }

        public int GetHashCode(Type obj)
        {
            throw new NotImplementedException();
        }
    }

    public static MethodInfo GetGenericMethod(this Type type, string name, Type[] parameterTypes)
    {
        var methods = type.GetMethods();
        foreach (var method in methods.Where(m => m.Name == name))
        {
            var methodParameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();

            if (methodParameterTypes.SequenceEqual(parameterTypes, new SimpleTypeComparer()))
            {
                return method;
            }
        }

        return null;
    }
}

With this in hand the following code will work:

typeof(Enumerable).GetGenericMethod("Where", new Type[] { typeof(IEnumerable<>), typeof(Func<,>) });
Dustin Campbell
  • 9,747
  • 2
  • 31
  • 32
  • Or in one line, return `type.GetMethods().FirstOrDefault(m => m.Name == name && m.GetParameters().Select(p => p.ParameterType).SequenceEqual(parameterTypes, new SimpleTypeComparer()));` :) Adding `params` to the last parameter would be nicer. – nawfal Oct 09 '13 at 07:44
  • Instead of newing up a SimpleTypeComparer each time through the loop make it a static field of the class for efficiency – reggaeguitar Feb 26 '21 at 21:57
  • This doesn't filter nested generics correctly. – Steve Andrews Nov 23 '22 at 18:23