web-dev-qa-db-fra.com

Réflexion pour identifier les méthodes d'extension

En C #, existe-t-il une technique utilisant la réflexion pour déterminer si une méthode a été ajoutée à une classe en tant que méthode d'extension?

Étant donné une méthode d'extension telle que celle présentée ci-dessous, est-il possible de déterminer que Reverse () a été ajouté à la classe string?

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

Nous recherchons un mécanisme pour déterminer, lors des tests unitaires, que la méthode d'extension a été ajoutée de manière appropriée par le développeur. Une raison de tenter cela est qu’il est possible que le développeur ajoute une méthode similaire à la classe réelle et, le cas échéant, le compilateur choisira cette méthode.

66
Mike Chess

Vous devez rechercher dans tous les assemblys où la méthode d'extension peut être définie.

Recherchez les classes décorées avec ExtensionAttribute , puis les méthodes de cette classe qui aussi sont décorées avec ExtensionAttribute. Ensuite, vérifiez le type du premier paramètre pour voir s'il correspond au type qui vous intéresse.

Voici un code complet. Cela pourrait être plus rigoureux (il ne vérifie pas que le type n'est pas imbriqué, ou qu'il existe au moins un paramètre), mais il devrait vous donner un coup de main.

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;
    }
}
102
Jon Skeet

Sur la base de la réponse de John Skeet, j'ai créé ma propre extension au type 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>();
        }
    }
}

Il récupère tous les assemblys de l'AppDomain actuel et recherche des méthodes étendues.

Usage:

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

La prochaine étape consisterait à étendre System.Type avec des méthodes, qui renverraient toutes les méthodes (également les méthodes "normales" avec celles étendues)

9

Ceci retournera une liste de toutes les méthodes d'extension définies dans un certain type, y compris les génériques:

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

Il n'y a qu'un seul problème avec cela: le type renvoyé n'est pas le même que celui attendu avec typeof (..), car il s'agit d'un type de paramètre générique. Pour trouver toutes les méthodes d'extension pour un type donné, vous devez comparer le GUID de tous les types de base et interfaces du type, comme:

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

Pour être complet:

Je garde un dictionnaire des objets type info, que je construis lors de l'itération de tous les types de tous les assemblys:

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

TypeInfo est défini comme suit:

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
drake7707

Pour clarifier un point, Jon a survolé ... "Ajouter" une méthode d'extension à une classe ne change en rien la classe. C'est juste un peu de spinning effectué par le compilateur C #. 

Donc, en utilisant votre exemple, vous pouvez écrire

string rev = myStr.Reverse();

mais le MSIL écrit à l’Assemblée sera exactement comme si vous l’aviez écrit:

string rev = StringExtensions.Reverse(myStr);

Le compilateur vous permet simplement de vous leurrer en pensant que vous appelez une méthode de chaîne.

3
James Curran

Une raison de tenter cela est qu’il est possible que le développeur ajoute une méthode similaire à la classe réelle et, le cas échéant, le compilateur choisira cette méthode.

  • Supposons qu'une méthode d'extension void Foo (ce client à un client) est définie.
  • Supposons également que le client soit modifié et que la méthode void Foo () soit ajoutée.
  • Ensuite, la nouvelle méthode sur Client couvrira/cachera la méthode d’extension.

La seule façon d'appeler l'ancienne méthode Foo à ce stade est la suivante:

CustomerExtension.Foo(myCustomer);
2
Amy B
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;
    }
}

Les sorties:

False

Vrai

0
billy