¿Cómo enumerar todas las clases con un atributo de clase personalizado?

151

Pregunta basada en el ejemplo de MSDN .

Digamos que tenemos algunas clases de C # con HelpAttribute en una aplicación de escritorio independiente. ¿Es posible enumerar todas las clases con dicho atributo? ¿Tiene sentido reconocer las clases de esta manera? El atributo personalizado se usaría para enumerar las posibles opciones de menú, al seleccionar el elemento se mostrará en la pantalla la instancia de dicha clase. El número de clases / artículos crecerá lentamente, pero de esta manera podemos evitar enumerarlos en otros lugares, creo.

tomash
fuente

Respuestas:

205

Si, absolutamente. Usando la reflexión:

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
fuente
77
De acuerdo, pero en este caso podemos hacerlo declarativamente según la solución de casperOne. Es agradable ser capaz de utilizar el rendimiento, es aún más agradable no tener que :)
Jon Skeet
9
Me gusta LINQ Me encanta, en realidad. Pero se necesita una dependencia de .NET 3.5, que no ofrece rendimiento. Además, LINQ finalmente se descompone esencialmente en lo mismo que el rendimiento. Entonces, ¿qué has ganado? Una sintaxis particular de C #, que es una preferencia.
Andrew Arnott
1
@AndrewArnott Fewest y las líneas de código más cortas son irrelevantes para el rendimiento, solo son posibles contribuyentes a la legibilidad y facilidad de mantenimiento. Desafío la afirmación de que asignan la menor cantidad de objetos y el rendimiento será más rápido (especialmente sin pruebas empíricas); básicamente ha escrito el Selectmétodo de extensión, y el compilador generará una máquina de estado tal como lo haría si llamara Selectdebido a su uso de yield return. Finalmente, cualquier ganancia de rendimiento que se pueda obtener en la mayoría de los casos sería micro optimizaciones.
casperOne
1
Muy bien, @casperOne. Una diferencia muy pequeña, especialmente en comparación con el peso de la reflexión misma. Probablemente nunca saldría en una traza perf.
Andrew Arnott
1
Por supuesto, Resharper dice "que el bucle foreach se puede convertir en una expresión LINQ" que se parece a esto: assembly.GetTypes (). Where (type => type.GetCustomAttributes (typeof (HelpAttribute), true) .Length> 0);
David Barrows,
107

Bueno, tendría que enumerar todas las clases en todos los ensamblados que se cargan en el dominio de la aplicación actual. Para hacer eso, llamaría al GetAssembliesmétodo en la AppDomaininstancia para el dominio de la aplicación actual.

Desde allí, llamaría GetExportedTypes(si solo desea tipos públicos) o GetTypesen cada uno Assemblypara obtener los tipos contenidos en el ensamblado.

Luego, llamaría al GetCustomAttributesmétodo de extensión en cada Typeinstancia, pasando el tipo de atributo que desea encontrar.

Puede usar LINQ para simplificar esto para usted:

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 consulta anterior le proporcionará cada tipo con su atributo aplicado, junto con la instancia de los atributos asignados a él.

Tenga en cuenta que si tiene una gran cantidad de ensamblados cargados en el dominio de su aplicación, esa operación podría ser costosa. Puede usar Parallel LINQ para reducir el tiempo de la operación, así:

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

Filtrarlo en un específico Assemblyes 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>() };

Y si el ensamblaje tiene una gran cantidad de tipos, puede usar Parallel LINQ nuevamente:

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
fuente
1
Enumerar todos los tipos en todos los ensamblajes cargados sería muy lento y no ganaría mucho. También es potencialmente un riesgo de seguridad. Probablemente pueda predecir qué ensamblajes contendrán los tipos que le interesan. Simplemente enumere los tipos en ellos.
Andrew Arnott
@Andrew Arnott: Correcto, pero esto es lo que se pidió. Es bastante fácil reducir la consulta para un ensamblado en particular. Esto también tiene el beneficio adicional de proporcionarle la asignación entre el tipo y el atributo.
casperOne
1
Puede usar el mismo código solo en el ensamblaje actual con System.Reflection.Assembly.GetExecutingAssembly ()
Chris Moschini
@ChrisMoschini Sí, puede, pero es posible que no siempre desee escanear el ensamblaje actual. Mejor dejarlo abierto.
casperOne
Lo he hecho muchas veces y no hay muchas maneras de hacerlo eficiente. Puede omitir los ensamblados de microsoft (están firmados con la misma clave, por lo que es bastante fácil evitarlos usando AssemblyName. Puede almacenar en caché los resultados dentro de una estática, que es exclusiva del AppDomain en el que se cargan los ensamblajes (tiene que almacenar en caché todo el contenido) nombres de los ensamblados que verificó en caso de que otros se hayan cargado mientras tanto). Me encontré aquí mientras estoy investigando el almacenamiento en caché de instancias cargadas de un tipo de atributo dentro del atributo. No estoy seguro de ese patrón, no estoy seguro de cuándo se instancian, etc.
34

Otras respuestas hacen referencia a GetCustomAttributes . Agregar este como ejemplo de uso de IsDefined

Assembly assembly = ...
var typesWithHelpAttribute = 
        from type in assembly.GetTypes()
        where type.IsDefined(typeof(HelpAttribute), false)
        select type;
Peatón imprudente
fuente
3
Creo que es la solución adecuada que utiliza el método previsto marco.
Alexey Omelchenko el
11

Como ya se dijo, la reflexión es el camino a seguir. Si va a llamar a esto con frecuencia, le recomiendo almacenar en caché los resultados, ya que la reflexión, especialmente la enumeración en cada clase, puede ser bastante lenta.

Este es un fragmento de mi código que se ejecuta en todos los tipos en todos los ensamblados cargados:

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

Esta es una mejora del rendimiento además de la solución aceptada. Iterar aunque todas las clases pueden ser lentas porque hay muchas. A veces puede filtrar un conjunto completo sin mirar ninguno de sus tipos.

Por ejemplo, si está buscando un atributo que usted mismo declaró, no espera que ninguna de las DLL del sistema contenga ningún tipo con ese atributo. La propiedad Assembly.GlobalAssemblyCache es una forma rápida de verificar las DLL del sistema. Cuando probé esto en un programa real, descubrí que podía omitir 30,101 tipos y solo tengo que verificar 1,983 tipos.

Otra forma de filtrar es usar Assembly.ReferencedAssemblies. Presumiblemente, si desea clases con un atributo específico, y ese atributo se define en un ensamblaje específico, entonces solo le importa ese ensamblaje y otros ensamblajes que hacen referencia a él. En mis pruebas, esto ayudó un poco más que verificar la propiedad GlobalAssemblyCache.

Combiné ambos y lo obtuve aún más rápido. El siguiente código incluye ambos 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)
Ideas comerciales Philip
fuente
4

En el caso de las limitaciones de Portable .NET , el siguiente código debería 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;
    }

o para una gran cantidad de ensamblajes que utilizan el estado de bucle 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
fuente
0

Podemos mejorar la respuesta de Andrew y convertir todo en una consulta LINQ.

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