Reflexion zur Identifizierung von Erweiterungsmethoden

77

Gibt es in C # eine Technik, bei der mithilfe von Reflexion ermittelt wird, ob einer Klasse eine Methode als Erweiterungsmethode hinzugefügt wurde?

Kann bei einer Erweiterungsmethode wie der unten gezeigten festgestellt werden, dass Reverse () zur Zeichenfolgenklasse hinzugefügt wurde?

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

Wir suchen nach einem Mechanismus, mit dem beim Testen von Einheiten festgestellt werden kann, dass die Erweiterungsmethode vom Entwickler entsprechend hinzugefügt wurde. Ein Grund, dies zu versuchen, besteht darin, dass es möglich ist, dass der Entwickler der tatsächlichen Klasse eine ähnliche Methode hinzufügt, und wenn dies der Fall ist, wird der Compiler diese Methode aufgreifen.

Mike Schach
quelle

Antworten:

117

Sie müssen in allen Assemblys suchen, in denen die Erweiterungsmethode definiert werden kann.

Suchen Sie nach Klassen, die mit dekoriert sind ExtensionAttribute, und dann nach Methoden innerhalb dieser Klasse, die ebenfalls mit dekoriert sind ExtensionAttribute. Überprüfen Sie dann den Typ des ersten Parameters, um festzustellen, ob er mit dem Typ übereinstimmt, an dem Sie interessiert sind.

Hier ist ein vollständiger Code. Es könnte strenger sein (es wird nicht überprüft, ob der Typ nicht verschachtelt ist oder ob es mindestens einen Parameter gibt), aber es sollte Ihnen helfen.

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;
    }
}
Jon Skeet
quelle
5
Schöner Code. Sie können eine Reihe von Methoden ausschließen, indem Sie Folgendes verwenden: Erweiterungsmethoden müssen in einer nicht generischen statischen Klasse definiert werden. where! type.IsGenericType && type.IsSealed
Amy B
Das ist zwar ein ziemlich einfacher Test. Es ist eine Schande, dass es kein Äquivalent zu IsDefined for Type gibt :) Ich werde die Antwort mit diesem Code bearbeiten.
Jon Skeet
Funktioniert nicht, wenn der erweiterte Typ generisch ist. Erweiterungsmethoden für IQueryable stimmen überein, nicht jedoch für IQueryable <>. Ich denke, es schlägt auf dem ParameterType fehl.
Seb Nilsson
4
@Seb: Ja, es würde ziemlich viel mehr Aufwand erfordern, damit es für generische Methoden funktioniert. Machbar, aber knifflig.
Jon Skeet
1
@ JonSkeet hmmm ich verstehe. Aber dann bleibt die Option, dass jeder das [Extension]Attribut auf eine Methode in einer statischen nicht generischen Klasse der obersten Ebene anwenden kann und trotzdem keine Erweiterungsmethode ist :) Nicht wahr?
Nawfal
12

Basierend auf der Antwort von John Skeet habe ich meine eigene Erweiterung für den System.Type-Typ erstellt.

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

Es ruft alle Assemblys aus der aktuellen AppDomain ab und sucht nach erweiterten Methoden.

Verwendung:

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

Der nächste Schritt wäre, System.Type um Methoden zu erweitern, die alle Methoden zurückgeben (auch die "normalen" mit den erweiterten).

Wolfgang Stelzhammer
quelle
5

Dadurch wird eine Liste aller in einem bestimmten Typ definierten Erweiterungsmethoden zurückgegeben, einschließlich der generischen:

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

Hier gibt es nur ein Problem: Der zurückgegebene Typ ist nicht derselbe, den Sie mit typeof (..) erwarten würden, da es sich um einen generischen Parametertyp handelt. Um alle Erweiterungsmethoden für einen bestimmten Typ zu finden, müssen Sie die GUID aller Basistypen und Schnittstellen des Typs wie folgt vergleichen:

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

Vollständig sein:

Ich behalte ein Wörterbuch mit Typinfoobjekten, das ich beim Iterieren aller Typen aller Assemblys erstelle:

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

wo das TypeInfodefiniert ist als:

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; }
}
drake7707
quelle
Derzeit fügt Ihr Code mehrere identische KVPs für mehrere Einschränkungen hinzu, und die "offensichtliche" Korrektur (die für einzelne Einschränkungen tatsächlich korrekter ist) des Hinzufügens (constraint, m}ist nicht richtig, da die Einschränkungen "und" nicht "oder" sind.
Mark Hurd
Aus Sicht des Compilers hat der aktuelle Code einige Vorteile, da er Einschränkungen nicht als Unterscheidungsmerkmale betrachtet: Löschen Sie einfach die foreach-Einschränkungszeile. Aber das hilft uns nicht, Erweiterungsmethoden durch Reflektion zu implementieren :-(
Mark Hurd
3

Um einen Punkt zu verdeutlichen, den Jon beschönigt hat ... Das Hinzufügen einer Erweiterungsmethode zu einer Klasse ändert die Klasse in keiner Weise. Es ist nur ein bisschen Spinning, das vom C # -Compiler ausgeführt wird.

Anhand Ihres Beispiels können Sie also schreiben

string rev = myStr.Reverse();

Die in die Assembly geschriebene MSIL ist jedoch genau so, als hätten Sie sie geschrieben:

string rev = StringExtensions.Reverse(myStr);

Der Compiler lässt Sie sich nur täuschen, dass Sie eine String-Methode aufrufen.

James Curran
quelle
3
Ja. Ich bin mir völlig bewusst, dass der Compiler etwas "Magie" arbeitet, um die Details zu verbergen. Dies ist einer der Gründe, warum wir in einem Komponententest feststellen möchten, ob es sich bei der Methode um eine Erweiterungsmethode handelt oder nicht.
Mike Chess
2

Ein Grund, dies zu versuchen, besteht darin, dass es möglich ist, dass der Entwickler der tatsächlichen Klasse eine ähnliche Methode hinzufügt, und wenn dies der Fall ist, wird der Compiler diese Methode übernehmen.

  • Angenommen, eine Erweiterungsmethode void Foo (dieser Kunde someCustomer) ist definiert.
  • Angenommen, der Kunde wird geändert und die Methode void Foo () hinzugefügt.
  • Dann wird die Erweiterungsmethode durch die neue Methode des Kunden abgedeckt / ausgeblendet.

Die einzige Möglichkeit, die alte Foo-Methode zu diesem Zeitpunkt aufzurufen, ist:

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

Ausgänge:

Falsch

Wahr

Billy
quelle