Как перечислять все классы с пользовательским атрибутом класса?

Вопрос основан на примере MSDN.

Допустим, у нас есть несколько классов С# с HelpAttribute в автономном настольном приложении. Можно ли перечислить все классы с таким атрибутом? Имеет ли смысл распознавать классы таким образом? Пользовательский атрибут будет использоваться для отображения списка возможных вариантов меню, выбор пункта приведет к выводу на экран экземпляра такого класса. Количество классов/предметов будет расти медленно, но, таким образом, мы можем избежать их перечисления в другом месте, я думаю.

Ответ 1

Да, абсолютно. Использование Reflection:

static IEnumerable<Type> GetTypesWithHelpAttribute(Assembly assembly) {
    foreach(Type type in assembly.GetTypes()) {
        if (type.GetCustomAttributes(typeof(HelpAttribute), true).Length > 0) {
            yield return type;
        }
    }
}

Ответ 2

Что ж, вам нужно будет перечислить все классы во всех сборках, которые загружены в текущий домен приложения. Для этого необходимо вызвать метод GetAssemblies в экземпляре AppDomain для текущего домена приложения.

Оттуда вы будете вызывать GetExportedTypes (если вам нужны только открытые типы) или GetTypes для каждой Assembly чтобы получить типы, содержащиеся в сборке.

Затем вы должны вызывать GetCustomAttributes расширения GetCustomAttributes для каждого экземпляра Type, передавая тип атрибута, который вы хотите найти.

Вы можете использовать LINQ, чтобы упростить это для вас:

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

Приведенный выше запрос даст вам каждый тип с примененным к нему атрибутом, а также экземпляр присвоенного ему атрибута (ов).

Обратите внимание: если в домен приложения загружено большое количество сборок, эта операция может быть дорогой. Вы можете использовать Parallel LINQ для сокращения времени выполнения операции, например:

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

Фильтрация его по конкретной Assembly проста:

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

И если в сборке много типов, вы можете снова использовать 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>() };

Ответ 3

Другие ответы ссылаются на GetCustomAttributes. Добавление этого примера в качестве примера использования IsDefined

Assembly assembly = ...
var typesWithHelpAttribute = 
        from type in assembly.GetTypes()
        where type.IsDefined(typeof(HelpAttribute), false)
        select type;

Ответ 4

Как уже говорилось, рефлексия - это путь. Если вы собираетесь называть это часто, я настоятельно рекомендую кэшировать результаты, поскольку отражение, особенно перечисление через каждый класс, может быть довольно медленным.

Это фрагмент кода, который проходит через все типы во всех загруженных сборках:

// 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.
        }
    }
}

Ответ 5

Это повышение производительности поверх принятого решения. Итерация, хотя все классы могут быть медленными, потому что их так много. Иногда вы можете отфильтровать всю сборку, не глядя на какие-либо ее типы.

Например, если вы ищете атрибут, который вы объявили сами, вы не ожидаете, что какая-либо из системных DLL будет содержать любые типы с этим атрибутом. Свойство Assembly.GlobalAssemblyCache - это быстрый способ проверки системных DLL. Когда я попробовал это в реальной программе, я обнаружил, что могу пропустить 30,101 типов, и мне нужно проверить только 1,983 типа.

Другой способ фильтрации - использовать Assembly.ReferencedAssemblies. Предположительно, если вы хотите, чтобы классы с определенным атрибутом, и этот атрибут определен в конкретной сборке, вам нужно только заботиться об этой сборке и других сборках, которые ссылаются на нее. В моих тестах это помогло немного больше, чем проверка свойства GlobalAssemblyCache.

Я объединил оба из них и получил его еще быстрее. Код ниже включает оба фильтра.

        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)

Ответ 6

В случае переносных ограничений .NET следующий код должен работать:

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

или для большого количества сборок с использованием основанного на контуре состояния 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;
                }
            }
        }
    }

Ответ 7

Мы можем улучшить ответ Andrew и преобразовать все это в один запрос LINQ.

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