Getmethod for Generic Method

GetMethod for generic method

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

How do you call GetMethod for a generic function that takes a generic parameter (without using GetMethods)?

It isn't readily available, because the types you need are actually generic type arguments that only exist in the method / parameter definition. For example, in your Test<T>(T val), the parameter type is "the T as defined by Test<T>. You can't construct that, because it isn't composed from anything. The only way to obtain that T is via GetParameters().

Basically, that leaves: the hard way - i.e. manually. For example:

var method1 = typeof(Program).GetMethods(flags).Single(x => x.Name == "Test"
&& x.IsGenericMethodDefinition && x.GetParameters().Length == 1
&& x.GetParameters()[0].ParameterType == x.GetGenericArguments()[0])
.MakeGenericMethod(paramTypes1);

Obviously it is simpler if you know there is only one Test(...) method:

var method = typeof(Program).GetMethod("Test", flags)
.MakeGenericMethod(paramTypes1);

Type.GetMethod() for generic method

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.

Type[] for GetMethod to get generic method with two arguments and one type parameter

You can use Type.MakeGenericSignatureType and pass Type.MakeGenericMethodParameter(0) to it as generic parameters:

var methodInfo = typeof(MyClass)
.GetMethod(nameof(MyClass.AddLink), new[]
{
Type.MakeGenericSignatureType(typeof(UnityAction<>), Type.MakeGenericMethodParameter(0)),
Type.MakeGenericSignatureType(typeof(UnityEvent<>), Type.MakeGenericMethodParameter(0))
});

Demo

C#: GetMethod by type (generic list)

Short version: typeof(IEnumerable<>) is not the same as typeof(IEnumerable<T>) (for some T).

Longer version: there is no method void c.m(IEnumerable<> Ls), only overloads where the generic parameter will be some specific – existing at run time – type where the jitter has needed to create the method due to some code referencing that instantiation of the generic method.

Add a call, in your test code, to some instance of the generic method and then do a GetMethod for that instance.

Consider the following:

using System.Collections.Generic;
using System.Linq;
using static System.Console;

class Methods {
public static void M(int x) {
// no-op
}

public static void M<T>(IEnumerable<T> x) {
// no-op
}
}

class Program {
static void Main(string[] args) {
Methods.M(0);
Methods.M(new[] { "a", "b" });

ShowAllM();
}

public static void ShowAllM() {
var tm = typeof(Methods);
foreach (var mi in tm.GetMethods().Where(m => m.Name == "M"))
{
WriteLine(mi.Name);
foreach (var p in mi.GetParameters())
{
WriteLine($"\t{p.ParameterType.Name}");
}
}
}
}

which produces the output:


M
Int32
M
IEnumerable`1

Note there is only one result from the generic overload. If a call to M<char>(…) is added to Main then the output is the same.

For reflection there is just one method, are its argument reflects its "open generic" nature, but that isn't quite the same as being callable with an open generic type (eg. IEnumerable<>) as open types are not instantiatable.

(I've fudged much of the technical details here. It is instruictive to look at the difference in a debugger between typeof(IEnumerable<>) and typeof(IEnumerable<int>).)

How to get MethodInfo for a generic method by name and its generic parameters?

The parameter type can't be the generic type definition. It has a type argument - which is the type parameter for the method. You can get that type parameter from MethodInfo.GetGenericArguments(), and then use it with typeof(Reference<>).MakeGenericType(...) to get the expected parameter type.

Here's a complete example, basically adapting your original code:

using System;
using System.Reflection;

class Reference<T> {}
interface IDomainObject {}

interface IRepository
{
string GetId<T>(T obj) where T : IDomainObject;
string GetId<T>(Reference<T> reference) where T : IDomainObject;
}

class Test
{
static void Main()
{
var method = GetReferenceAcceptingGetIdMethod();
Console.WriteLine(method);
}

public static MethodInfo GetReferenceAcceptingGetIdMethod()
{
var repositoryInterfaceType = typeof(IRepository);
foreach (var m in repositoryInterfaceType.GetMethods())
{
if (m.IsGenericMethodDefinition && m.Name == nameof(IRepository.GetId))
{
var typeParameter = m.GetGenericArguments()[0];
var expectedParameterType = typeof(Reference<>).MakeGenericType(typeParameter);
var parameters = m.GetParameters();
if (parameters.Length == 1)
{
var firstParamType = parameters[0].ParameterType;
if (firstParamType == expectedParameterType)
{
return m;
}
}
}
}
throw new Exception();
}
}

How to resolve generic method ambiguity in C# reflection

If you don't want to use Type.GetMethods and iterate over the results you can use this overload of GetMethod and Type.MakeGenericMethodParameter which is available from .NET Core 2.1:

class MyClass
{
// Argument types
public class Bar<T> { }
public class Bar<T, U> { }

// Generic method overrides
private static void Foo<T>(Bar<T> b) { }
private static void Foo<T, U>(Bar<T, U> b) { }
}

typeof(MyClass)
.GetMethod(
"Foo",
1,
BindingFlags.NonPublic | BindingFlags.Static,
null,
new[] { Type.MakeGenericSignatureType(typeof(MyClass.Bar<>), Type.MakeGenericMethodParameter(0)) },
null
); // First Foo

typeof(MyClass)
.GetMethod(
"Foo",
2,
BindingFlags.NonPublic | BindingFlags.Static,
null,
new[] { Type.MakeGenericSignatureType(typeof(MyClass.Bar<,>), Type.MakeGenericMethodParameter(0),Type.MakeGenericMethodParameter(1)) },
null
); // Second Foo

UPD

Since Type.MakeGenericMethodParameter is not available for you the only option I see (if every method is not a part of generic class with the same generic type arguments) is to use GetMethods and filter the results. Simplest filter in this case would be number of generic parameters;

var secondMethod = typeof(MyClass).GetMethods(BindingFlags.NonPublic | BindingFlags.Static)
.Where(mi => mi.Name == "Foo" && mi.GetGenericArguments().Length == 2)
.First();

How to get MethodInfo for generic extension method?

What is the correct way to get the MethodInfo?

You have to find the generic method - which is unfortunately a bit of a pain - and then construct that with the appropriate arguments. In this case you know that there are only 2 Contains overloads, and the one you want has two arguments, so you can use:

var method = typeof(Enumerable).GetMethods()
.Where(m => m.Name == "Contains")
.Single(m => m.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T));

You should then be able to invoke it appropriately.



Related Topics



Leave a reply



Submit