Como enumerar todas as classes com atributo de classe personalizada?

151

Pergunta baseada no exemplo do MSDN .

Digamos que tenhamos algumas classes de C # com HelpAttribute no aplicativo de desktop independente. É possível enumerar todas as classes com esse atributo? Faz sentido reconhecer classes dessa maneira? O atributo personalizado seria usado para listar as opções de menu possíveis, a seleção do item trará para a instância de tela dessa classe. O número de classes / itens aumentará lentamente, mas dessa forma podemos evitar enumerá-los em outros lugares, eu acho.

tomash
fonte

Respostas:

205

Sim absolutamente. Usando Reflexão:

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
fonte
7
Concordou, mas neste caso, podemos fazê-lo declarativamente conforme a solução do casperOne. É bom ser capaz de usar o rendimento, é ainda melhor para não ter de :)
Jon Skeet
9
Eu gosto do LINQ. Ame, na verdade. Mas é preciso uma dependência do .NET 3.5, que gera retorno não. Além disso, o LINQ finalmente se decompõe para essencialmente a mesma coisa que o retorno do rendimento. Então, o que você ganhou? Uma sintaxe específica do C #, que é uma preferência.
23413 Andrew Arnott
1
@AndrewArnott As linhas de código mais curtas e menores são irrelevantes para o desempenho, são apenas possíveis contribuintes para a legibilidade e a manutenção. Eu desafio a afirmação de que eles alocam o menor número de objetos e o desempenho será mais rápido (especialmente sem prova empírica); você basicamente escreveu o Selectmétodo de extensão, e o compilador gerará uma máquina de estado como faria se você ligasse Selectpor causa do uso de yield return. Finalmente, quaisquer ganhos de desempenho que possam ser obtidos na maioria dos casos são micro-otimizações.
precisa saber é o seguinte
1
Muito bem, @casperOne. Uma diferença muito pequena, especialmente em comparação com o peso da própria reflexão. Provavelmente nunca chegaria a um ponto perfeito.
Andrew Arnott 27/07
1
É claro que o Resharper diz "que o loop foreach pode ser convertido em uma expressão LINQ", que se parece com isso: assembly.GetTypes (). Where (type => type.GetCustomAttributes (typeof (HelpAttribute), true) .Length> 0);
David Barrows
107

Bem, você teria que enumerar todas as classes em todos os assemblies carregados no domínio do aplicativo atual. Para fazer isso, você chamaria o GetAssembliesmétodo na AppDomaininstância para o domínio de aplicativo atual.

A partir daí, você chamaria GetExportedTypes(se quiser apenas tipos públicos) ou GetTypesem cada um Assemblypara obter os tipos que estão contidos na montagem.

Em seguida, você chamaria o GetCustomAttributesmétodo de extensão em cada Typeinstância, passando o tipo do atributo que deseja encontrar.

Você pode usar o LINQ para simplificar isso para você:

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

A consulta acima fornecerá a você cada tipo com seu atributo aplicado a ela, juntamente com a instância do (s) atributo (s) atribuído (s) a ela.

Observe que, se você tiver um grande número de assemblies carregados no domínio do aplicativo, essa operação poderá ser cara. Você pode usar o LINQ paralelo para reduzir o tempo da operação, assim:

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

Filtrando-o em um específico Assembly é simples:

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

E se a montagem tiver um grande número de tipos, você poderá usar o Parallel LINQ novamente:

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
fonte
1
Enumerar todos os tipos em todos os assemblies carregados seria muito lento e não lhe renderia muito. Também é potencialmente um risco à segurança. Você provavelmente pode prever quais assemblies conterão os tipos nos quais você está interessado. Apenas enumere os tipos nesses.
31813 Andrew Arnott
@ Andrew Arnott: Correto, mas é isso que foi solicitado. É fácil o suficiente para remover a consulta para um assembly específico. Isso também tem o benefício adicional de fornecer o mapeamento entre o tipo e o atributo.
casperOne
1
Você pode usar o mesmo código apenas na montagem atual com System.Reflection.Assembly.GetExecutingAssembly ()
Chris Moschini
@ChrisMoschini Sim, você pode, mas nem sempre você pode verificar a montagem atual. Melhor deixar em aberto.
usar o seguinte código
Já fiz isso várias vezes e não há muitas maneiras de torná-lo eficiente. Você pode pular os assemblies da Microsoft (eles são assinados com a mesma chave, portanto é muito fácil evitar o uso de AssemblyName. Você pode armazenar em cache os resultados em uma estática, que é exclusiva do AppDomain no qual os assemblies são carregados (é necessário armazenar em cache o valor completo). nomes dos assemblies que você verificou caso outros sejam carregados nesse meio tempo.) Encontrei-me aqui enquanto investigava as instâncias carregadas em cache de um tipo de atributo dentro do atributo. Não tenho certeza desse padrão, não tenho certeza de quando são instanciadas, etc.
34

Outras respostas referenciam GetCustomAttributes . Adicionando este como um exemplo do uso de IsDefined

Assembly assembly = ...
var typesWithHelpAttribute = 
        from type in assembly.GetTypes()
        where type.IsDefined(typeof(HelpAttribute), false)
        select type;
Jay Walker
fonte
3
Eu acredito que é a solução adequada que usa o método de framework pretendido.
Alexey Omelchenko #
11

Como já foi dito, a reflexão é o caminho a percorrer. Se você vai chamar isso com frequência, sugiro que os resultados sejam armazenados em cache, pois a reflexão, especialmente enumerando todas as classes, pode ser bastante lenta.

Este é um trecho do meu código que percorre todos os tipos em todos os assemblies carregados:

// 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
fonte
9

Este é um aprimoramento de desempenho sobre a solução aceita. A iteração, embora todas as classes possam ser lentas, porque existem muitas. Às vezes, você pode filtrar uma montagem inteira sem observar nenhum de seus tipos.

Por exemplo, se você estiver procurando por um atributo que você mesmo declarou, não espera que nenhuma das DLLs do sistema contenha nenhum tipo com esse atributo. A propriedade Assembly.GlobalAssemblyCache é uma maneira rápida de verificar as DLLs do sistema. Quando tentei isso em um programa real, descobri que podia pular 30.101 tipos e só tenho que verificar 1.983 tipos.

Outra maneira de filtrar é usar Assembly.ReferencedAssemblies. Presumivelmente, se você deseja classes com um atributo específico, e esse atributo é definido em uma montagem específica, você só se preocupa com essa montagem e outras montagens que a referenciam. Nos meus testes, isso ajudou um pouco mais do que a verificação da propriedade GlobalAssemblyCache.

Combinei os dois e consegui ainda mais rápido. O código abaixo inclui os dois filtros.

        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)
Philip Trade-Ideas
fonte
4

No caso de limitações Portable .NET , o seguinte código deve funcionar:

    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 para um grande número de montagens usando o loop-state 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
fonte
0

Podemos melhorar a resposta de Andrew e converter tudo em uma consulta LINQ.

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