82

In C# is there a technique using reflection to determine if a method has been added to a class as an extension method?

Given an extension method such as the one shown below is it possible to determine that Reverse() has been added to the string class?

public static class StringExtensions
{
    public static string Reverse(this string value)
    {
        char[] cArray = value.ToCharArray();
        Array.Reverse(cArray);
        return new string(cArray);
    }
}

We're looking for a mechanism to determine in unit testing that the extension method was appropriately added by the developer. One reason to attempt this is that it is possible that a similar method would be added to the actual class by the developer and, if it was, the compiler will pick that method up.

M.Babcock
  • 18,753
  • 6
  • 54
  • 84
Mike Chess
  • 2,758
  • 5
  • 29
  • 37

7 Answers7

126

You have to look in all the assemblies where the extension method may be defined.

Look for classes decorated with ExtensionAttribute, and then methods within that class which are also decorated with ExtensionAttribute. Then check the type of the first parameter to see if it matches the type you're interested in.

Here's some complete code. It could be more rigorous (it's not checking that the type isn't nested, or that there is at least one parameter) but it should give you a helping hand.

using System;
using System.Runtime.CompilerServices;
using System.Reflection;
using System.Linq;
using System.Collections.Generic;

public static class FirstExtensions
{
    public static void Foo(this string x) { }
    public static void Bar(string x) { } // Not an ext. method
    public static void Baz(this int x) { } // Not on string
}

public static class SecondExtensions
{
    public static void Quux(this string x) { }
}

public class Test
{
    static void Main()
    {
        Assembly thisAssembly = typeof(Test).Assembly;
        foreach (MethodInfo method in GetExtensionMethods(thisAssembly, typeof(string)))
        {
            Console.WriteLine(method);
        }
    }
    static IEnumerable<MethodInfo> GetExtensionMethods(Assembly assembly, Type extendedType)
    {
        var isGenericTypeDefinition = extendedType.IsGenericType && extendedType.IsTypeDefinition;
        var query = from type in assembly.GetTypes()
            where type.IsSealed && !type.IsGenericType && !type.IsNested
            from method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
            where method.IsDefined(typeof(ExtensionAttribute), false)
            where isGenericTypeDefinition
                ? method.GetParameters()[0].ParameterType.IsGenericType && method.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == extendedType
                : method.GetParameters()[0].ParameterType == extendedType
            select method;
        return query;
    }
}
Despacito 2
  • 444
  • 2
  • 10
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 5
    Nice code. You could rule out a bunch of methods using the fact that: Extension methods must be defined in a non-generic static class. where !type.IsGenericType && type.IsSealed – Amy B Nov 18 '08 at 18:58
  • True, that's a reasonably simple test. It's a shame that there isn't an equivalent of IsDefined for Type :) I'll edit the answer with that code. – Jon Skeet Nov 18 '08 at 19:14
  • Doesn't work if the extended type is a generic. Extension-methods for IQueryable will match, but not for IQueryable<>, for example. I think it fails on the ParameterType. – Seb Nilsson Nov 16 '09 at 18:27
  • 4
    @Seb: Yes, it would take a fair amount more effort to make it work for generic methods. Feasible, but tricky. – Jon Skeet Nov 16 '09 at 19:00
  • @JonSkeet why would you want the `type.IsSealed && !type.IsGenericType && !type.IsNested` checking? Wouldn't checking for just the extension attribute on `MethodInfo` do here? – nawfal Oct 20 '13 at 20:11
  • 1
    @nawfal: Well the C# compiler does that kind of checking - it's not an extension method if it's not in a top-level static non-generic class... anyone can apply the `[Extension]` attribute to a method - that doesn't make it an extension method if those conditions don't apply. – Jon Skeet Oct 20 '13 at 20:36
  • 1
    @JonSkeet hmmm I get it. But then it leaves the option that anyone can apply the `[Extension]` attribute to a method in a a top-level static non-generic class and still not be an extension method :) Isnt it? – nawfal Oct 20 '13 at 20:39
  • 1
    @nawfal: Well at that point it's an extension method as far as the C# compiler is concerned - the compiler will accept it as an extension method even if it wasn't compiled from code with a `this` parameter. Recognizing it in IL in the same way as the compiler does is as close as you'll get. – Jon Skeet Oct 20 '13 at 21:04
  • @JonSkeet Ok I get it. But now I'm doubtful of what you said: "anyone can apply the [Extension] attribute to a method" Is that possible? I tried one now. Compiler forces the `this` keyword, which will force all other criteria to be met. – nawfal Oct 20 '13 at 21:11
  • 1
    @nawfal: Interesting - just tried it myself and you're right. That's a new restriction; older compilers didn't have a problem with that. But you could still construct IL that way either from other languages or directly *as* IL. My point is that the code in my answer is still effectively mimicking what the C# compiler does to detect extension methods. – Jon Skeet Oct 21 '13 at 05:46
13

Based on John Skeet's answer I've created my own extension to the System.Type-type.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace System
{
    public static class TypeExtension
    {
        /// <summary>
        /// This Methode extends the System.Type-type to get all extended methods. It searches hereby in all assemblies which are known by the current AppDomain.
        /// </summary>
        /// <remarks>
        /// Insired by Jon Skeet from his answer on http://stackoverflow.com/questions/299515/c-sharp-reflection-to-identify-extension-methods
        /// </remarks>
        /// <returns>returns MethodInfo[] with the extended Method</returns>

        public static MethodInfo[] GetExtensionMethods(this Type t)
        {
            List<Type> AssTypes = new List<Type>();

            foreach (Assembly item in AppDomain.CurrentDomain.GetAssemblies())
            {
                AssTypes.AddRange(item.GetTypes());
            }

            var query = from type in AssTypes
                where type.IsSealed && !type.IsGenericType && !type.IsNested
                from method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
                where method.IsDefined(typeof(ExtensionAttribute), false)
                where method.GetParameters()[0].ParameterType == t
                select method;
            return query.ToArray<MethodInfo>();
        }

        /// <summary>
        /// Extends the System.Type-type to search for a given extended MethodeName.
        /// </summary>
        /// <param name="MethodeName">Name of the Methode</param>
        /// <returns>the found Methode or null</returns>
        public static MethodInfo GetExtensionMethod(this Type t, string MethodeName)
        {
            var mi = from methode in t.GetExtensionMethods()
                where methode.Name == MethodeName
                select methode;
            if (mi.Count<MethodInfo>() <= 0)
                return null;
            else
                return mi.First<MethodInfo>();
        }
    }
}

It get's all assemblies from the current AppDomain and searches for extended methods.

Usage:

Type t = typeof(Type);
MethodInfo[] extendedMethods = t.GetExtensionMethods();
MethodInfo extendedMethodInfo = t.GetExtensionMethod("GetExtensionMethods");

The next step would be to extend System.Type with methods, which returns all Methods (also the "normal" ones with the extended ones)

M.Babcock
  • 18,753
  • 6
  • 54
  • 84
Stelzi79
  • 585
  • 3
  • 12
5

This will return a list of all extension methods defined in a certain type, including the generic ones:

public static IEnumerable<KeyValuePair<Type, MethodInfo>> GetExtensionMethodsDefinedInType(this Type t)
{
    if (!t.IsSealed || t.IsGenericType || t.IsNested)
        return Enumerable.Empty<KeyValuePair<Type, MethodInfo>>();

    var methods = t.GetMethods(BindingFlags.Public | BindingFlags.Static)
                   .Where(m => m.IsDefined(typeof(ExtensionAttribute), false));

    List<KeyValuePair<Type, MethodInfo>> pairs = new List<KeyValuePair<Type, MethodInfo>>();
    foreach (var m in methods)
    {
        var parameters = m.GetParameters();
        if (parameters.Length > 0)
        {
            if (parameters[0].ParameterType.IsGenericParameter)
            {
                if (m.ContainsGenericParameters)
                {
                    var genericParameters = m.GetGenericArguments();
                    Type genericParam = genericParameters[parameters[0].ParameterType.GenericParameterPosition];
                    foreach (var constraint in genericParam.GetGenericParameterConstraints())
                        pairs.Add(new KeyValuePair<Type, MethodInfo>(parameters[0].ParameterType, m));
                }
            }
            else
                pairs.Add(new KeyValuePair<Type, MethodInfo>(parameters[0].ParameterType, m));
        }
    }

    return pairs;
}

There's only one problem with this: The Type returned is not the same you'd expect with typeof(..), because it's a generic parameter type. In order to find all the extension methods for a given type you'll have to compare the GUID of all the base types and interfaces of the Type like:

public List<MethodInfo> GetExtensionMethodsOf(Type t)
{
    List<MethodInfo> methods = new List<MethodInfo>();
    Type cur = t;
    while (cur != null)
    {

        TypeInfo tInfo;
        if (typeInfo.TryGetValue(cur.GUID, out tInfo))
            methods.AddRange(tInfo.ExtensionMethods);


        foreach (var iface in cur.GetInterfaces())
        {
            if (typeInfo.TryGetValue(iface.GUID, out tInfo))
                methods.AddRange(tInfo.ExtensionMethods);
        }

        cur = cur.BaseType;
    }
    return methods;
}

To be complete:

I keep a dictionary of type info objects, that I build when iterating all the types of all assemblies:

private Dictionary<Guid, TypeInfo> typeInfo = new Dictionary<Guid, TypeInfo>();

where the TypeInfo is defined as:

public class TypeInfo
{
    public TypeInfo()
    {
        ExtensionMethods = new List<MethodInfo>();
    }

    public List<ConstructorInfo> Constructors { get; set; }

    public List<FieldInfo> Fields { get; set; }
    public List<PropertyInfo> Properties { get; set; }
    public List<MethodInfo> Methods { get; set; }

    public List<MethodInfo> ExtensionMethods { get; set; }
}
nawfal
  • 70,104
  • 56
  • 326
  • 368
Drakarah
  • 2,244
  • 2
  • 23
  • 23
  • As it stands, your code adds multiple identical KVPs for multiple constraints, and the "obvious" fix (which is actually more correct for single constraints) of adding `(constraint, m}` isn't right because constraints are "and" not "or". – Mark Hurd May 07 '16 at 16:44
  • Actually, from the point of view of the compiler, because it doesn't consider constraints as differentiators, the current code has some merit: just drop the foreach constraint line. But that doesn't help us implementing extension methods via reflection :-( – Mark Hurd May 07 '16 at 18:05
3

To clarify a point Jon glossed over... "Adding" an extension method to a class does not change the class in any way. It's just a little bit of spinning performed by the C# compiler.

So, using your example, you may write

string rev = myStr.Reverse();

but the MSIL written to the assembly will be exactly as if you had written it:

string rev = StringExtensions.Reverse(myStr);

The compiler is merely letting you fool yourself into thinking you are calling an method of String.

James Curran
  • 101,701
  • 37
  • 181
  • 258
  • 3
    Yes. I'm completely aware that the compiler is working some "magic" to hide the details. That's one of the reasons we're interested in detecting in a unit test whether the method is an extension method or not. – Mike Chess Nov 18 '08 at 18:14
2

One reason to attempt this is that it is possible that a similar method would be added to the actual class by the developer and, if it was, the compiler will pick that method up.

  • Suppose an extension method void Foo(this Customer someCustomer) is defined.
  • Suppose, also, that Customer is modified and the method void Foo() is added.
  • Then, the new method on Customer will cover/hide the extension method.

The only way to call the old Foo method at that point is:

CustomerExtension.Foo(myCustomer);
Amy B
  • 108,202
  • 21
  • 135
  • 185
0
void Main()
{
    var test = new Test();
    var testWithMethod = new TestWithExtensionMethod();
    Tools.IsExtensionMethodCall(() => test.Method()).Dump();
    Tools.IsExtensionMethodCall(() => testWithMethod.Method()).Dump();
}

public class Test 
{
    public void Method() { }
}

public class TestWithExtensionMethod
{
}

public static class Extensions
{
    public static void Method(this TestWithExtensionMethod test) { }
}

public static class Tools
{
    public static MethodInfo GetCalledMethodInfo(Expression<Action> expr)
    {
        var methodCall = expr.Body as MethodCallExpression;
        return methodCall.Method;
    }

    public static bool IsExtensionMethodCall(Expression<Action> expr)
    {
        var methodInfo = GetCalledMethodInfo(expr);
        return methodInfo.IsStatic;
    }
}

Outputs:

False

True

billy
  • 1,165
  • 9
  • 23
0

This is the solution using LINQ method syntax rather than query syntax based on @Jon Skeet's answer.

public static IEnumerable<MethodInfo> GetExtensionMethods(Assembly assembly, Type extendedType)
{
    var methods = assembly.GetTypes()
        .Where(type => type.IsSealed && !type.IsGenericType && !type.IsNested)
        .SelectMany(type => type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
        .Where(method => method.IsDefined(typeof(ExtensionAttribute), false) && 
                         method.GetParameters()[0].ParameterType == extendedType);
    return methods;
}
iBener
  • 469
  • 6
  • 18