Comment énumérer toutes les classes avec un attribut de classe personnalisé?

151

Question basée sur l' exemple MSDN .

Disons que nous avons des classes C # avec HelpAttribute dans une application de bureau autonome. Est-il possible d'énumérer toutes les classes avec un tel attribut? Est-il judicieux de reconnaître les classes de cette façon? L'attribut personnalisé serait utilisé pour lister les options de menu possibles, la sélection d'un élément amènera à l'écran l'instance de cette classe. Le nombre de classes / éléments augmentera lentement, mais de cette façon, nous pouvons éviter de les énumérer tous ailleurs, je pense.

Tomash
la source

Réponses:

205

Oui absolument. Utilisation de la réflexion:

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
la source
7
D'accord, mais dans ce cas, nous pouvons le faire de manière déclarative selon la solution de casperOne. C'est bien de pouvoir utiliser le rendement, c'est encore plus agréable de ne pas avoir à le faire :)
Jon Skeet
9
J'aime LINQ. J'adore ça, en fait. Mais il faut une dépendance sur .NET 3.5, ce qui n'est pas le cas. En outre, LINQ se décompose finalement en essentiellement la même chose que le rendement du rendement. Alors, qu'avez-vous gagné? Une syntaxe C # particulière, c'est une préférence.
Andrew Arnott
1
@AndrewArnott Les lignes de code les plus courtes et les plus courtes ne sont pas pertinentes pour les performances, ce ne sont que des contributeurs possibles à la lisibilité et à la maintenabilité. Je conteste l'affirmation selon laquelle ils allouent le moins d'objets et les performances seront plus rapides (surtout sans preuve empirique); vous avez essentiellement écrit la Selectméthode d'extension, et le compilateur générera une machine à états comme il le ferait si vous appeliez à Selectcause de votre utilisation de yield return. Enfin, les gains de performances qui pourraient être obtenus dans la majorité des cas sont des micro-optimisations.
casperOne
1
Tout à fait raison, @casperOne. Une très petite différence, surtout par rapport au poids de réflexion lui-même. Il ne viendrait probablement jamais dans une trace de perf.
Andrew Arnott
1
Bien sûr, Resharper dit "que la boucle foreach peut être convertie en une expression LINQ" qui ressemble à ceci: assembly.GetTypes (). Where (type => type.GetCustomAttributes (typeof (HelpAttribute), true) .Length> 0);
David Barrows
107

Eh bien, vous devrez énumérer toutes les classes de tous les assemblys chargés dans le domaine d'application actuel. Pour ce faire, vous appelleriez la GetAssembliesméthode sur l' AppDomaininstance pour le domaine d'application actuel.

À partir de là, vous appelleriez GetExportedTypes(si vous ne voulez que des types publics) ou GetTypessur chacun Assemblyd' eux pour obtenir les types contenus dans l'assembly.

Ensuite, vous appelleriez la GetCustomAttributesméthode d'extension sur chaque Typeinstance, en passant le type de l'attribut que vous souhaitez trouver.

Vous pouvez utiliser LINQ pour simplifier cela pour vous:

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

La requête ci-dessus vous obtiendra chaque type avec votre attribut appliqué, ainsi que l'instance du ou des attributs qui lui sont attribués.

Notez que si vous avez un grand nombre d'assemblys chargés dans votre domaine d'application, cette opération peut être coûteuse. Vous pouvez utiliser Parallel LINQ pour réduire la durée de l'opération, comme ceci:

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

Le filtrer sur un spécifique Assemblyest simple:

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

Et si l'assembly contient un grand nombre de types, vous pouvez à nouveau utiliser Parallel LINQ:

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
la source
1
Énumérer tous les types dans tous les assemblys chargés serait juste très lent et ne vous rapporterait pas beaucoup. C'est aussi potentiellement un risque de sécurité. Vous pouvez probablement prédire quels assemblys contiendront les types qui vous intéressent. Énumérez simplement les types de ceux-ci.
Andrew Arnott
@Andrew Arnott: C'est exact, mais c'est ce qui a été demandé. Il est assez facile d'élaguer la requête pour un assembly particulier. Cela a également l'avantage supplémentaire de vous donner le mappage entre le type et l'attribut.
casperOne
1
Vous pouvez utiliser le même code uniquement sur l'assembly actuel avec System.Reflection.Assembly.GetExecutingAssembly ()
Chris Moschini
@ChrisMoschini Oui, vous pouvez, mais vous ne voudrez peut-être pas toujours analyser l'assemblage actuel. Mieux vaut le laisser ouvert.
casperOne
Je l'ai fait plusieurs fois et il n'y a pas beaucoup de façons de le rendre efficace. Vous pouvez ignorer les assemblys Microsoft (ils sont signés avec la même clé, il est donc assez facile d'éviter d'utiliser AssemblyName. Vous pouvez mettre en cache les résultats dans un fichier statique, qui est unique à l'AppDomain dans lequel les assemblys sont chargés (vous devez mettre en cache le noms des assemblys que vous avez vérifiés au cas où d'autres seraient chargés entre-temps). Je me suis retrouvé ici alors que j'étudiais la mise en cache des instances chargées d'un type d'attribut dans l'attribut. Je ne suis pas sûr de ce modèle, je ne sais pas quand ils sont instanciés, etc.
34

D'autres réponses font référence à GetCustomAttributes . Ajout de celui-ci comme exemple d'utilisation d' IsDefined

Assembly assembly = ...
var typesWithHelpAttribute = 
        from type in assembly.GetTypes()
        where type.IsDefined(typeof(HelpAttribute), false)
        select type;
Jay Walker
la source
3
Je crois que c'est la bonne solution qui utilise la méthode prévue par le cadre.
Alexey Omelchenko
11

Comme déjà indiqué, la réflexion est la voie à suivre. Si vous appelez cela fréquemment, je suggère fortement de mettre en cache les résultats, car la réflexion, en particulier l'énumération de chaque classe, peut être assez lente.

Ceci est un extrait de mon code qui traverse tous les types dans tous les assemblys chargés:

// 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.
        }
    }
}
Codage avec Spike
la source
9

Il s'agit d'une amélioration des performances en plus de la solution acceptée. Itérer toutes les classes peut être lent car il y en a tellement. Parfois, vous pouvez filtrer un assemblage entier sans regarder aucun de ses types.

Par exemple, si vous recherchez un attribut que vous avez déclaré vous-même, vous ne vous attendez pas à ce que l'une des DLL système contienne des types avec cet attribut. La propriété Assembly.GlobalAssemblyCache est un moyen rapide de rechercher les DLL système. Quand j'ai essayé cela sur un vrai programme, j'ai découvert que je pouvais sauter 30 101 types et que je n'avais qu'à vérifier 1 983 types.

Une autre façon de filtrer consiste à utiliser Assembly.ReferencedAssemblies. Vraisemblablement, si vous voulez des classes avec un attribut spécifique et que cet attribut est défini dans un assembly spécifique, vous ne vous souciez que de cet assembly et des autres assemblys qui le référencent. Dans mes tests, cela m'a aidé un peu plus que la vérification de la propriété GlobalAssemblyCache.

J'ai combiné les deux et je l'ai obtenu encore plus rapidement. Le code ci-dessous comprend les deux filtres.

        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)
Commerce-Idées Philip
la source
4

En cas de limitations de Portable .NET , le code suivant devrait fonctionner:

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

ou pour un grand nombre d'assemblys utilisant des états de boucle 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
la source
0

Nous pouvons améliorer la réponse d'Andrew et convertir le tout en une seule requête LINQ.

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