Отражение для определения методов расширения

В С# есть метод, использующий отражение, чтобы определить, был ли метод добавлен к классу в качестве метода расширения?

Для метода расширения, такого как приведенный ниже, можно определить, что Reverse() был добавлен в класс строк?

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

Мы ищем механизм для определения в модульном тестировании того, что разработчик расширил метод расширения. Одна из причин, по которой это делается, состоит в том, что возможно, что подобный метод будет добавлен в фактический класс разработчиком, и если бы это было так, то компилятор подберет этот метод.

Ответ 1

Вы должны посмотреть во всех сборках, где может быть определен метод расширения.

Ищите классы, украшенные ExtensionAttribute, а затем методы в этом классе, которые также украшены ExtensionAttribute. Затем проверьте тип первого параметра, чтобы узнать, соответствует ли он интересующему вас типу.

Вот полный код. Он может быть более строгим (он не проверяет, что тип не вложен, или что есть хотя бы один параметр), но он должен дать вам руку помощи.

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 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 method.GetParameters()[0].ParameterType == extendedType
                    select method;
        return query;
    }
}

Ответ 2

На основании ответа Джона Скита я создал собственное расширение для типа System.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>();
        }
    }
}

Получает все сборки из текущего AppDomain и ищет расширенные методы.

Использование:

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

Следующим шагом будет расширение System.Type с помощью методов, которые возвращают все методы (также "обычные" с расширенными)

Ответ 3

Это вернет список всех методов расширения, определенных в определенном типе, включая общие:

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;
}

Есть только одна проблема с этим: Возвращаемый тип не является тем же, который вы ожидаете с помощью typeof (..), потому что это общий тип параметра. Чтобы найти все методы расширения для данного типа, вам нужно будет сравнить GUID всех базовых типов и интерфейсов типа 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;
}

Чтобы быть полным:

Я сохраняю словарь объектов типа info, которые я создаю при повторении всех типов всех сборок:

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

где TypeInfo определяется как:

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; }
}

Ответ 4

Чтобы прояснить точку, которую Джон замалчивал... "Добавление" метода расширения в класс не меняет класс каким-либо образом. Это просто немного поворота, выполняемого компилятором С#.

Итак, используя ваш пример, вы можете написать

string rev = myStr.Reverse();

но MSIL, записанный на сборку, будет точно таким, как если бы вы его написали:

string rev = StringExtensions.Reverse(myStr);

Компилятор просто позволяет вам обманывать себя, думая, что вы вызываете метод String.

Ответ 5

Одна из причин попытки этого заключается в том, что возможно, что подобный метод будет добавлен в фактический класс разработчиком, и если бы это было так, то компилятор подберет этот метод.

  • Предположим, что определен метод расширения void Foo (этот Customer someCustomer).
  • Предположим также, что Клиент изменен и добавлен метод void Foo().
  • Затем новый метод для Клиента будет охватывать/скрывать метод расширения.

Единственный способ вызова старого метода Foo в этой точке:

CustomerExtension.Foo(myCustomer);

Ответ 6

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;
    }
}

Выходы:

False

True