Wie werden alle Klassen mit benutzerdefinierten Klassenattributen aufgelistet?

151

Frage basierend auf MSDN-Beispiel .

Angenommen, wir haben einige C # -Klassen mit HelpAttribute in einer eigenständigen Desktopanwendung. Ist es möglich, alle Klassen mit einem solchen Attribut aufzulisten? Ist es sinnvoll, Klassen so zu erkennen? Ein benutzerdefiniertes Attribut wird verwendet, um mögliche Menüoptionen aufzulisten. Wenn Sie ein Element auswählen, wird die Instanz einer solchen Klasse auf dem Bildschirm angezeigt. Die Anzahl der Klassen / Gegenstände wird langsam zunehmen, aber auf diese Weise können wir vermeiden, sie alle an anderer Stelle aufzuzählen, denke ich.

Tomash
quelle

Antworten:

205

Ja absolut. Verwenden der Reflexion:

static IEnumerable<Type> GetTypesWithHelpAttribute(Assembly assembly) {
    foreach(Type type in assembly.GetTypes()) {
        if (type.GetCustomAttributes(typeof(HelpAttribute), true).Length > 0) {
            yield return type;
        }
    }
}
Andrew Arnott
quelle
7
Einverstanden, aber in diesem Fall können wir dies deklarativ gemäß der Lösung von casperOne tun. Es ist schön, Ertrag nutzen zu können, es ist noch schöner, nicht zu müssen :)
Jon Skeet
9
Ich mag LINQ. Ich liebe es eigentlich. Es ist jedoch eine Abhängigkeit von .NET 3.5 erforderlich, die keine Rendite liefert. Außerdem zerfällt LINQ schließlich im Wesentlichen auf das Gleiche wie die Rendite. Was hast du gewonnen? Eine bestimmte C # -Syntax, das ist eine Präferenz.
Andrew Arnott
1
@AndrewArnott Wenigste und kürzeste Codezeilen sind für die Leistung irrelevant. Sie tragen nur zur Lesbarkeit und Wartbarkeit bei. Ich fordere die Aussage heraus, dass sie die wenigsten Objekte zuweisen und die Leistung schneller sein wird (insbesondere ohne empirischen Beweis). Sie haben im Grunde die SelectErweiterungsmethode geschrieben , und der Compiler generiert eine Zustandsmaschine genau so, als ob Sie Selectaufgrund Ihrer Verwendung von aufgerufen hätten yield return. Schließlich alle Performance - Gewinne , die möglicherweise in der Mehrzahl der Fälle sein Mikro-Optimierungen erzielt werden.
CasperOne
1
Ganz richtig, @casperOne. Ein sehr kleiner Unterschied, insbesondere im Vergleich zum Gewicht der Reflexion. Wahrscheinlich würde nie in einer Perf-Spur auftauchen.
Andrew Arnott
1
Natürlich sagt Resharper, "dass jede Schleife in einen LINQ-Ausdruck konvertiert werden kann", was folgendermaßen aussieht: Assembly.GetTypes (). Where (type => type.GetCustomAttributes (typeof (HelpAttribute), true) .Length> 0);
David Barrows
107

Nun, Sie müssten alle Klassen in allen Assemblys auflisten, die in die aktuelle App-Domäne geladen werden. Dazu rufen Sie die GetAssembliesMethode auf der AppDomainInstanz für die aktuelle App-Domäne auf.

Von dort aus würden Sie GetExportedTypes(wenn Sie nur öffentliche Typen möchten) oder GetTypesauf jedem Assemblydie Typen aufrufen , die in der Assembly enthalten sind.

Anschließend rufen Sie die GetCustomAttributesErweiterungsmethode für jede TypeInstanz auf und übergeben den Typ des Attributs, das Sie suchen möchten.

Sie können LINQ verwenden, um dies für Sie zu vereinfachen:

var typesWithMyAttribute =
    from a in AppDomain.CurrentDomain.GetAssemblies()
    from t in a.GetTypes()
    let attributes = t.GetCustomAttributes(typeof(HelpAttribute), true)
    where attributes != null && attributes.Length > 0
    select new { Type = t, Attributes = attributes.Cast<HelpAttribute>() };

Mit der obigen Abfrage erhalten Sie jeden Typ, auf den Ihr Attribut angewendet wurde, sowie die Instanz der ihm zugewiesenen Attribute.

Beachten Sie, dass dieser Vorgang teuer sein kann, wenn Sie eine große Anzahl von Assemblys in Ihre Anwendungsdomäne geladen haben. Sie können Parallel LINQ verwenden , um die Operationszeit wie folgt zu verkürzen:

var typesWithMyAttribute =
    // Note the AsParallel here, this will parallelize everything after.
    from a in AppDomain.CurrentDomain.GetAssemblies().AsParallel()
    from t in a.GetTypes()
    let attributes = t.GetCustomAttributes(typeof(HelpAttribute), true)
    where attributes != null && attributes.Length > 0
    select new { Type = t, Attributes = attributes.Cast<HelpAttribute>() };

Das Filtern nach einem bestimmten Assemblyist einfach:

Assembly assembly = ...;

var typesWithMyAttribute =
    from t in assembly.GetTypes()
    let attributes = t.GetCustomAttributes(typeof(HelpAttribute), true)
    where attributes != null && attributes.Length > 0
    select new { Type = t, Attributes = attributes.Cast<HelpAttribute>() };

Wenn die Assembly eine große Anzahl von Typen enthält, können Sie Parallel LINQ erneut verwenden:

Assembly assembly = ...;

var typesWithMyAttribute =
    // Partition on the type list initially.
    from t in assembly.GetTypes().AsParallel()
    let attributes = t.GetCustomAttributes(typeof(HelpAttribute), true)
    where attributes != null && attributes.Length > 0
    select new { Type = t, Attributes = attributes.Cast<HelpAttribute>() };
casperOne
quelle
1
Das Aufzählen aller Typen in allen geladenen Assemblys wäre nur sehr langsam und würde Ihnen nicht viel bringen. Es ist auch möglicherweise ein Sicherheitsrisiko. Sie können wahrscheinlich vorhersagen, welche Assemblys die Typen enthalten, an denen Sie interessiert sind. Zählen Sie einfach die Typen in diesen auf.
Andrew Arnott
@ Andrew Arnott: Richtig, aber darum wurde gebeten. Es ist einfach genug, die Abfrage für eine bestimmte Assembly zu reduzieren. Dies hat auch den zusätzlichen Vorteil, dass Sie die Zuordnung zwischen dem Typ und dem Attribut erhalten.
casperOne
1
Sie können den gleichen Code nur für die aktuelle Assembly mit System.Reflection.Assembly.GetExecutingAssembly ()
Chris Moschini
@ChrisMoschini Ja, das können Sie, aber Sie möchten möglicherweise nicht immer die aktuelle Assembly scannen. Lass es lieber offen.
CasperOne
Ich habe das schon oft gemacht und es gibt nicht viele Möglichkeiten, es effizient zu machen. Sie können Microsoft-Assemblys überspringen (sie sind mit demselben Schlüssel signiert, sodass die Verwendung von AssemblyName recht einfach vermieden werden kann. Sie können Ergebnisse in einer statischen Datei zwischenspeichern, die nur für die AppDomain gilt, in der die Assemblys geladen werden (sie müssen vollständig zwischengespeichert werden) Namen der Assemblys, die Sie überprüft haben, falls andere in der Zwischenzeit geladen wurden). Ich befand mich hier, als ich das Zwischenspeichern geladener Instanzen eines Attributtyps innerhalb des Attributs untersuche. Ich bin mir dieses Musters nicht sicher, nicht sicher, wann sie instanziiert werden usw.
34

Andere Antworten verweisen auf GetCustomAttributes . Hinzufügen dieses Beispiels für die Verwendung von IsDefined

Assembly assembly = ...
var typesWithHelpAttribute = 
        from type in assembly.GetTypes()
        where type.IsDefined(typeof(HelpAttribute), false)
        select type;
Jay Walker
quelle
3
Ich halte es für die richtige Lösung, die die vom Framework beabsichtigte Methode verwendet.
Alexey Omelchenko
11

Wie bereits erwähnt, ist Reflexion der richtige Weg. Wenn Sie dies häufig aufrufen, empfehle ich dringend, die Ergebnisse zwischenzuspeichern, da die Reflexion, insbesondere die Aufzählung in jeder Klasse, sehr langsam sein kann.

Dies ist ein Ausschnitt aus meinem Code, der alle Typen in allen geladenen Assemblys durchläuft:

// this is making the assumption that all assemblies we need are already loaded.
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) 
{
    foreach (Type type in assembly.GetTypes())
    {
        var attribs = type.GetCustomAttributes(typeof(MyCustomAttribute), false);
        if (attribs != null && attribs.Length > 0)
        {
            // add to a cache.
        }
    }
}
CodingWithSpike
quelle
9

Dies ist eine Leistungssteigerung zusätzlich zu der akzeptierten Lösung. Das Iterieren, obwohl alle Klassen langsam sein können, weil es so viele gibt. Manchmal können Sie eine gesamte Baugruppe herausfiltern, ohne einen ihrer Typen zu betrachten.

Wenn Sie beispielsweise nach einem Attribut suchen, das Sie selbst deklariert haben, erwarten Sie nicht, dass eine der System-DLLs Typen mit diesem Attribut enthält. Mit der Assembly.GlobalAssemblyCache-Eigenschaft können Sie schnell nach System-DLLs suchen. Als ich dies mit einem echten Programm versuchte, stellte ich fest, dass ich 30.101 Typen überspringen konnte und nur 1.983 Typen überprüfen muss.

Eine andere Möglichkeit zum Filtern ist die Verwendung von Assembly.ReferencedAssemblies. Wenn Sie Klassen mit einem bestimmten Attribut möchten und dieses Attribut in einer bestimmten Assembly definiert ist, interessieren Sie sich vermutlich nur für diese Assembly und andere Assemblys, die darauf verweisen. In meinen Tests hat dies etwas mehr geholfen als das Überprüfen der GlobalAssemblyCache-Eigenschaft.

Ich habe beide kombiniert und es noch schneller bekommen. Der folgende Code enthält beide Filter.

        string definedIn = typeof(XmlDecoderAttribute).Assembly.GetName().Name;
        foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
            // Note that we have to call GetName().Name.  Just GetName() will not work.  The following
            // if statement never ran when I tried to compare the results of GetName().
            if ((!assembly.GlobalAssemblyCache) && ((assembly.GetName().Name == definedIn) || assembly.GetReferencedAssemblies().Any(a => a.Name == definedIn)))
                foreach (Type type in assembly.GetTypes())
                    if (type.GetCustomAttributes(typeof(XmlDecoderAttribute), true).Length > 0)
Handelsideen Philip
quelle
4

Im Fall der Einschränkungen für Portable .NET sollte der folgende Code funktionieren:

    public static IEnumerable<TypeInfo> GetAtributedTypes( Assembly[] assemblies, 
                                                           Type attributeType )
    {
        var typesAttributed =
            from assembly in assemblies
            from type in assembly.DefinedTypes
            where type.IsDefined(attributeType, false)
            select type;
        return typesAttributed;
    }

oder für eine große Anzahl von Assemblys, die den Schleifenstatus verwenden yield return:

    public static IEnumerable<TypeInfo> GetAtributedTypes( Assembly[] assemblies, 
                                                           Type attributeType )
    {
        foreach (var assembly in assemblies)
        {
            foreach (var typeInfo in assembly.DefinedTypes)
            {
                if (typeInfo.IsDefined(attributeType, false))
                {
                    yield return typeInfo;
                }
            }
        }
    }
Lorenz Lo Sauer
quelle
0

Wir können Andrews Antwort verbessern und das Ganze in eine LINQ-Abfrage umwandeln.

    public static IEnumerable<Type> GetTypesWithHelpAttribute(Assembly assembly)
    {
        return assembly.GetTypes().Where(type => type.GetCustomAttributes(typeof(HelpAttribute), true).Length > 0);
    }
Tachyon
quelle