Атрибуты C#: обо всех аспектах

    Здравствуй, читатель. В этой статье описаны атрибуты со всех сторон — начиная от спецификации, смысла и определения атрибутов, создания собственных и работе с ними, заканчивая добавлением атрибутов на рантайме и наиболее полезными и интересными существующими атрибутами. Если вам интересна тема атрибутов в C#, то добро пожаловать под кат.


    Содержание


    1. Введение. Определение и назначение атрибутов
    2. Интересные атрибуты с поддержкой от рантайма. Здесь будет дана краткая информация о разнообразных атрибутах, о существовании которых мало кто знает и еще меньше кто пользуется. Так как это абсолютно непрактичные сведения, много разглагольствований не будет (вопреки моей страсти к малоприменимым знаниям)
    3. Некоторые из малоизвестных атрибутов, которые полезно знать
    4. Определение своего атрибута и его обработка. Добавление атрибутов во время выполнения

    Введение


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

    Начать стоит с определения метаданных. Метаданные — данные, которые описывают и ссылаются на типы, определенные CTS. Метаданные хранятся таким образом, который не зависит от какого-либо конкретного языка программирования. Таким образом, метаданные предоставляют общий механизм обмена информацией о программе для использования между инструментами, которым она требуется (компиляторы и отладчики, а также сама программа), а также между VES. Метаданные входят в манифест сборки. Могут храниться в PE файле вместе с IL кодом или в отдельном PE файле, где будет только манифест сборки.
    Атрибут — это характеристика типа или его членов (или других конструкций языка), которая содержит описательную информацию. Хотя наиболее распространенные атрибуты предопределены и имеют определенный формат в метаданных, пользовательские атрибуты также могут быть добавлены к метаданным. Атрибуты коммутативны, т.е. порядок их объявления над элементом неважен

    С синтаксической точки зрения (в метаданных) есть следующие атрибуты

    1. Использующие специальный синтаксис в IL. Например, ключевые слова являются атрибутами. И для них существует специальный синтаксис в IL. Их довольно много, перечислять все не имеет смысла
    2. Использующие обобщенный синтаксис. К ним относятся пользовательские и библиотечные атрибуты
    3. Атрибуты безопасности. К ним относятся атрибуты, наследующиеся от SecurityAttribute (напрямую или косвенно). Они обрабатываются особым образом. Для них существует специальный синтаксис в IL, который позволяет создавать xml, описывающий эти атрибуты напрямую

    Пример


    Код на C# содержащий все вышеперечисленные виды атрибутов
    [StructLayout(LayoutKind.Explicit)]
    [Serializable]
    [Obsolete]
    [SecurityPermission(SecurityAction.Assert)]
    public class Sample
    {
    }
    


    Получившийся IL
    .class public EXPLICIT ansi SERIALIZABLE beforefieldinit
      AttributeSamples.Sample
        extends [System.Runtime]System.Object
    {
      .custom instance void [System.Runtime]System.ObsoleteAttribute::.ctor()
        = (01 00 00 00 )
      .permissionset assert  = {
        class 'System.Security.Permissions.SecurityPermissionAttribute, System.Runtime.Extensions, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' = {}}
    
      .method public hidebysig specialname rtspecialname instance void
        .ctor() cil managed {/*constructor body*/}
    }
    


    Как видно StructLayoutAttribute имеет специальный синтаксис, так как в IL он представлен как «explicit». ObsoleteAttribute использует общий синтаксис — в IL начинается с ".custom". SecurityPermissionAttribute как атрибут безопасности превратился в ".permissionset assert".

    Пользовательские атрибуты добавляют пользовательскую информацию к метаданным. Этот механизм может использоваться для хранения специфичной для приложения информации во время компиляции и для доступа к ней во время выполнения или для чтения и анализа другим инструментом. Хотя любой определенный пользователем тип может быть использован в качестве атрибута, для соответствия CLS необходимо, чтобы атрибуты наследовались от System.Attribute. CLI предопределяет некоторые атрибуты и использует их для управления поведением во время выполнения. Некоторые языки определяют атрибуты для представления возможностей языка, не представленных непосредственно в CTS.

    Как уже упоминалось, атрибуты хранятся в метаданных, которые, в свою очередь, формируются на этапе компиляции, т.е. вносятся в PE файл (как правило *.dll). Таким образом, добавить во время выполнения атрибут можно только модифицируя исполняемый файл во время выполнения (но времена самоизменяющихся программ уже давно в прошлом). Отсюда следует, что на этапе выполнения их добавить нельзя, но это не совсем точно. В случае, если мы формируем свою сборку, определяем в ней типы, то мы можем на этапе выполнения создать новый тип и вешать атрибуты на него. Так что формально, мы все же можем добавлять атрибуты на этапе выполнения (пример будет в самом низу).

    А теперь немного об ограничениях


    Если по каким-то причинам в одной сборке существует 2 атрибута с именами Name и NameAtribute, то становится невозможным поставить первый из них. При использовании [Name] (т.е. без суффикса) компилятор говорит, что видит неопределенность. При использовании [NameAttribute] мы поставим NameAttribute, что логично. Для такой мистической ситуации с недостатком воображения при именовании существует специальный синтаксис. Чтобы поставить первую версию без суффикса можно указать знак собаки (т.е. [Name] — шутка, так не надо) перед именем атрибута [@Name].

    Пользовательские атрибуты могут быть добавлены к чему угодно, кроме пользовательских атрибутов. Имеется в виду метаданные, т.е. если мы ставим в C# атрибут над классом атрибута, то в метаданных он будет относиться к классу. А вот добавить атрибут к «public» нельзя. Зато можно к сборкам, модулям, классам, типам значений, перечислениям (enum), конструкторам, методам, свойствам, полям, событиям, интерфейсам, параметрам, делегатам, возвращаемым значениям или обобщенным параметрам. В примере ниже приведены очевидные и не очень примеры того, как можно поставить атрибут на ту или иную конструкцию.

    Синтаксис объявления атрибутов
    using System;
    using System.Runtime.InteropServices;
    using System.Security.Permissions;
    using AttributeSamples;
    
    [assembly:All]
    [module:All]
    
    namespace AttributeSamples
    {
        [AttributeUsage(AttributeTargets.All)]
        public class AllAttribute : Attribute
        {
        }
    
        [All] // на класс
        public class Usage
        {
            [All] // на метод
            [return:All] // на возвращаемое методом значение
            public int GiveMeInt<[All]T>([All]int param)
            {
                return 5 + param;
            }
    
            [All] // на событие
            [field:All] // на сгенерированное поле делегата для этого события
            public event Action Event;
    
            [All] // на свойство
            [field: All] // на сгенерированное поле для этого свойства
            public int Action { get; set; }
        }
    }
    


    Атрибуты имеют 2 типа параметров — именованные и позиционные. К позиционным относятся параметры конструктора. К именованным — публичные свойства с доступным сеттером. При этом это не просто формальные названия, все параметры можно указывать при объявлении атрибута в скобках после его названия. Именованные не являются обязательными.

    Виды параметров
    [AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
    public class AttrParamsAttribute : Attribute
    {
        public AttrParamsAttribute(int positional) // позиционный
        {
        }
    
        public int Named { get; set; } // именованный
    }
    
    [AttrParams(1)]
    [AttrParams(1, Named = 2)]
    public class AttrParams { }
    


    Допустимые параметры (обоих видов) для атрибута должны быть одного из перечисленных типов:

    1. bool, byte, char, double, float, int, long, short, string и далее по примитивным, кроме decimal
    2. object
    3. System.Type
    4. enum
    5. Одномерный массив любого из вышеперечисленных типов

    Это во многом объясняется тем, что это должна быть констранта времени компиляции, а типы выше могут принимать эту константу, (принимая object мы можем передать int). Но по непонятной причине аргумент не может быть типа ValueType, хотя это и возможно с логической точки зрения.

    Существует два вида пользовательских атрибутов: подлинные пользовательские атрибуты (genuine custom attributes) и псевдо-пользовательские (pseudo-custom).
    В коде они выглядят одинаково (указываются над конструкцией языка в квадратных скобках), но обрабатываются по-разному:

    1. Подлинный пользовательский атрибут сохраняется непосредственно в метаданных; параметры атрибута хранятся как есть. Они доступны во время выполнения и сохраняются в виде набора байт (спешу напомнить, что они известны во время компиляции)
    2. Псевдо-пользовательский атрибут распознается, потому что его имя является одним из специального списка. Вместо того, чтобы хранить его данные непосредственно в метаданных, они анализируются и используются для установки битов или полей в таблицах метаданных, а данные затем сбрасываются и далее их получить нельзя. Таблицы метаданных проверяются во время выполнения быстрее, чем подлинные пользовательские атрибуты и при этом для хранения информации требуется меньше места.

    Псевдо-пользовательские атрибуты не видны рефлексии
    [Serializable]
    [StructLayout(LayoutKind.Explicit)]
    public class CustomPseudoCustom { }
    
    class Program
    {
        static void Main()
        {
    	    var onlyCustom = typeof(CustomPseudoCustom).GetCustomAttributes(); //только SerializableAttribute
        }
    }
    


    Большинство пользовательских атрибутов введены на уровне языка. Они хранятся и возвращаются рантаймом, при этом рантайм не знает ничего о значении этих атрибутов. Но все псевдо-пользовательские атрибуты плюс некоторые пользовательские атрибуты представляют особый интерес для компиляторов и для CLI. Таким образом мы переходим к следующему разделу.

    Атрибуты с поддержкой рантайма


    Данный раздел носит чисто информативный характер, если интереса к тому, что использует рантайм нет, то можно скролить к следующему разделу.

    В таблице ниже перечислены псевдо-пользовательские атрибуты и специальные пользовательские атрибуты (CLI или компиляторы обрабатывают их специальным образом).

    Псевдо-пользовательские атрибуты (их нельзя получить через рефлексию).
    CLI Атрибуты:
    Атрибут Описание
    AssemblyAlgorithmIDAttribute Записывает идентификатор используемого алгоритма хеширования. Задает поле Assembly.HashAlgId
    AssemblyFlagsAttribute Записывает флаги для соответствующей сборки. Задает поле Assembly.Flags
    DllImportAttribute Предоставляет информацию о коде, реализованном в неуправляемой библиотеке. Устанавливает Method.Flags.PinvokeImpl бит соответствующего метода; добавляет новую запись в ImplMap (устанавливая значения MappingFlags, MemberForwarded, ImportName и ImportScope)
    StructLayoutAttribute Позволяет явно задать способ размещения полей ссылочного или значимого типа. Устанавливает поле TypeDef.Flags.LayoutMask для типа. Также может устанавливать поля TypeDef.Flags.StringFormatMask, ClassLayout.PackingSize и ClassLayout.ClassSize
    FieldOffsetAttribute Определяет смещение в байтах полей в ссылочном или значимом типе. Устанавливает значение FieldLayout.OffSet для соответствующего метода
    InAttribute Показывает, что параметр передается как [in] аргумент. Устанавливает Param.Flags.In бит для соответствующего параметра
    OutAttribute Показывает, что параметр передается как [out] аргумент. Устанавливает Param.Flags.Out бит для соответствующего параметра
    MarshalAsAttribute Определяет способ маршалинга данных между управляемым и неуправляемым кодом. Устанавливает Field.Flags.HasFieldMarshal бит для поля (или Param.Flags.HasFieldMarshal бит для параметра); Добавляет запись в таблицу FieldMarshal (устанавливая значения Parent и NativeType)
    MethodImplAttribute Определяет детали реализации метода. Устанавливает значение Method.ImplFlags для соответствующего метода


    CLS Атрибуты — языки должны поддерживать их:
    Атрибут Описание
    AttributeUsageAttribute Используется для указания, как атрибут может быть использован
    ObsoleteAttribute Показывает, что элемент не должен использоваться
    CLSCompliantAttribute Указывает, объявлен ли элемент как CLS-совместимый

    Разное интересное
    Атрибут Описание
    ThreadStaticAttribute Предоставляет поля типа, относящиеся к потоку
    ConditionalAttribute Помечает метод как вызываемый, опираясь на условие компиляции (указанное в /define). Если условие не соблюдено, то метод не вызовется (И не будет скомпилирован в IL). Может быть помечен только void метод. В противном случае возникнет ошибка компиляции
    DecimalConstantAttribute Сохраняет значение константы типа decimal в метаданных
    DefaultMemberAttribute Определяет член класса, который будет использоваться по умолчанию методом InvokeMember
    CompilationRelaxationsAttribute Указывает, являются ли исключения из проверок инструкций строгими или смягченными. На текущий момент можно передать только параметр NoStringInterning, который помечает сборку как не требующую интернирования строковых литералов. Но этот механизм все еще может использоваться
    FlagsAttribute Атрибут, указывающий, должен ли enum восприниматься как битовые флаги
    IndexerNameAttribute Указывает имя, под которым индексатор будет известен в языках программирования, которые не поддерживают такую ​​возможность напрямую
    ParamArrayAttribute Показывает, что метод принимает переменное число параметров

    Полезные атрибуты


    Неотъемлемой частью разработки программного продукта является отладка. И зачастую в большой и сложной системе требуется десятки и сотни раз запускать один и тот же метод и наблюдать за состоянием объектов. При этом на раз 20 уже начинает конкретно бесить необходимость разворачивать один объект вглубь раз 400, чтоб увидеть значение одной переменной и перезапустить метод заново.
    Для более спокойной и быстрой отладки можно использовать атрибуты, модифицирующие поведение отладчика.

    DebuggerDisplayAttribute указывает, как отображается тип или его член в окне переменных отладчика (ну и не только).

    Единственным аргументом конструктора является строка с форматом отображения. То, что будет находиться между фигурными скобками, будет вычислено. Формат как у интерполированной строки, только без доллара. Нельзя использовать указатели в вычисляемом значении. Кстати, если у вас есть переопределенный ToString, то его значение будет показываться как если бы оно было в этом атрибуте. Если есть и ToString и атрибут, то значение берется из атрибута.


    DebuggerBrowsableAttribute определяет способ отображения поля или свойства в окне переменных отладчика. Принимает DebuggerBrowsableState, который имеет 3 опции:

    • Never — поле вообще не отображается во время отладки. При раскрытии иерархии объектов данное поле отображаться не будет
    • Collapsed — поле является нераскрытым, но его можно развернуть. Это поведение по умолчанию
    • RootHidden — само поле не показывается, но объекты, из которых оно состоит, показываются (для массивов и коллекций)



    DebuggerTypeProxy — если объект просматривается в отладчике сотни раз в день, то можно заморочиться и потратить минуты 3 на создание прокси объекта, который отобразит исходный объект как надо. Обычно прокси объект для отображения — внутренний класс. Собственно, он и будет отображаться вместо целевого объекта.



    Другие полезные атрибуты

    ThreadStatic — атрибут позволяющий сделать статическую переменную своей для каждого потока. Для этого надо поставить атрибут над статическим полем. Стоит помнить важный нюанс — инициализация статическим конструктором будет выполнена только один раз, и переменная поменяется в том потоке, который выполнит статический конструктор. В остальных она останется дефолтной. (ЗЫ. Если вам необходимо такое поведение, советую взглянуть в сторону класса ThreadLocal).

    Немного о подкапотных нюансах. И в Linux, и в Windows существует область памяти, локальная для потока (TLS и TSD соответственно). Однако сами по себе эти области очень невелики. Поэтому создается структура ThreadLocalInfo, указатель на которую помещается в TLS. Соответственно используется только один слот. Сама структура содержит 3 поля — Thread, AppDomain, ClrTlsInfo. Нас интересует первое. Именно оно организует хранение потокостатики в памяти, используя для этого ThreadLocalBlock и ThreadLocalModule.

    Таким образом:

    • Ссылочные типы — алоцированы в куче, ссылки на них держит ThreadStaticHandleTable, который поддерживается классом ThreadLocalBlock
    • Структуры — упакованы и хранятся в управляемой куче также как и ссылочные типы
    • Примитивные значимые типы хранятся в участках неуправляемой памяти, которая является частью ThreadLocalModule

    Ну и коль про это зашла речь, стоит упомянуть про асинхронные методы. Как внимательный читатель может заметить, в случае, если мы используем асинхронность, то продолжение далеко не обязательно выполнится в том же потоке (мы можем влиять на контекст исполнения, но не поток). Соответственно, мы получим лабуду, если будем использовать ThreadLocal. В данном случае рекомендуется использовать AsyncLocal. Но статья не про это, поэтому поехали дальше.

    InternalsVisibleTo — позволяет указать сборку, которой будут видны элементы, помеченные internal. Может показаться, что если какой-то сборке нужны определенные типы и их члены, можно просто пометить их public и не париться. Но хорошая архитектура подразумевает сокрытие деталей имплементации. Тем не менее они могут понадобиться для каких-нибудь инфраструктурных вещей, например, тестовых проектов. С помощью этого атрибута можно поддерживать и инкапсуляцию, и требуемый процент покрытия тестами.

    HandleProcessCorruptedStateExceptions — позволяет отпугивать робких программистов и ловить исключения поврежденного состояния. По умолчанию для таких исключений CLR не выполняет отловки. В общем случае лучшим выходом будет как раз позволить приложению упасть. Это опасные исключения, показывающие, что память процесса повреждена, так что использование данного атрибута — идея очень плохая. Но возможно в некоторых случаях, для локальной разработки будет полезно на некоторое время поставить данный атрибут. Чтобы словить исключение поврежденного состояния, достаточно просто поставить этот атрибут над методом. И если уже дошло до использования этого атрибута, то рекомендуется (впрочем, как и всегда) отлавливать какое-то конкретное исключение.

    DisablePrivateReflection — делает все приватные члены сборки недосягаемыми для рефлексии. Атрибут ставиться на сборку.

    Определение своего атрибута


    Не просто так этом раздел стоит последним. Ведь лучший способ понять, в каких случаях будет выгодно использовать атрибут — посмотреть на уже используемые. Сложно сказать формализованное правило, когда следует задуматься о собственном атрибуте. Зачастую их используют как дополнительную информацию о типе/его члене или другой конструкции языка, которая бывает общей у совершенно разных сущностей. Как пример — все атрибуты, используемые для сериализации/ОРМ/форматирования и тд. Из-за обширного применения данных механизмов к совершенно разным типам, зачастую не известным разработчикам соответствующего механизма, использование атрибутов — отличный способ дать пользователю возможность предоставлять декларативную информацию для данного механизма.

    Использование своих атрибутов можно разделить на 2 части:

    1. Создание атрибута и его использование
    2. Получение атрибута и его обработка

    Создание атрибута и его использование


    Для создания своего атрибута достаточно наследоваться от System.Attribute. При этом желательно придерживаться упомянутого стиля именования — заканчивать имя класса на Attribute. При этом никакой ошибки не будет, если опустить этот суффикс. Как упоминалось ранее, атрибуты могут иметь 2 типа параметров — позиционные и именованные. Логика их применения такая же, как и со свойствами и параметрами конструктора у класса — значения, необходимые для создания объекта, для которых не существует разумного «дефолта» выносятся в позиционные (т.е. конструктор). То, чему можно поставить какой-то разумный дефолт, который будет зачастую использован, лучше выделить в именованный (т.е. свойство).

    Немаловажным в создании атрибута является ограничение мест его применения. Для этого используется AttributeUsageAttribute. Обязательным параметром (позиционным) является AttributeTarget, определяющий место использования атрибута (метод, сборка и тд). Необязательными (именованными) параметрами являются:

    1. AllowMultiple — указывает, можно ли размещать более одного атрибута над местом его применения или нет. По умолчанию false
    2. Inherited — определяет, будет ли этот атрибут относится к классам наследникам (в случае размещения над базовым классом) и переопределенным методам (в случае размещения над методом). По умолчанию true.

    После этого можно нагружать атрибуты полезной нагрузкой. Атрибут — декларативная информация, значит все, что в нем определяется должно описывать конструкцию, к которой относится. Атрибут не должен содержать какой-то глубокой логики. За обработку определенных вами атрибутов должны отвечать специальные сервисы, которые как раз будут их обрабатывать. Но то, что в атрибуте не должно быть логики, не означает, что в нем не должно быть методов.

    Метод (функция) — тоже информация и тоже может описывать конструкцию. И используя полиморфизм в атрибутах можно предоставить весьма мощный и удобный инструмент, где пользователь сможет влиять как на информацию, используемую вашим инструментом, так и на определенные этапы выполнения и обработки. При этом ему не надо будет плодить классы, внедрять зависимости, стоить фабрики и их интерфейсы, которые будут создавать эти классы. Достаточно будет создать единственный класс-наследник, который инкапсулирует в себе детали работы с элементом, к которому он относится. Но, как правило, достаточно обычного «РОСО» атрибута с парой свойств.

    Получение и обработка атрибута


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

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

    Но все берет начало от интерфейса ICustomAttributeProvider. Его реализуют такие типы как Assembly, MemberInfo, Module, ParameterInfo. В свою очередь наследниками MemberInfo являются Type, EventInfo, FieldInfo, MethodBase, PropertyInfo.

    Интерфейс имеет лишь 3 функции, и они не очень удобные. Они работают с массивами (даже если мы знаем, что атрибут может быть лишь один) и не параметризованы типом (используют object). Поэтому напрямую к функциям этого интерфейса придется обращаться редко (не сказал никогда, потому что не хочу быть категоричным). Для удобства использования существует класс CustomAttributeExtensions, в котором собрано множество методов расширения для всевозможных типов, выполняющих несложные операции по приведению типов, выборке единственного значения и так далее, тем самым освобождая разработчика от данной необходимости. Также эти методы доступны как статические в классе Attribute с полезнейшей функцией игнорирования параметра inherit (для нонконформистов).

    Основные используемые функции приведены ниже. Первый параметр, указывающий какой тип расширяет метод, я опустил. Также везде, где указан параметр bool inherit существует перегрузка без него (со значением по умолчанию true). Этот параметр указывает, нужно ли учитывать при выполнении метода атрибуты родительского класса или базового метода (если используется на переопределенном методе). В случае, если в атрибуте inherit = flase, то даже установка его в true не поможет учитывать атрибуты базового класса
    Название метода Описание
    GetCustomAttributes<LogAttribute>(bool inherit) получает перечисление атрибутов указанного типа. Если атрибут один, вернется перечисление из 1 элемента
    GetCustomAttribute<LogAttribute>(bool inherit) возвращает единственный атрибут указанного типа. Если таких несколько, выбрасывает исключение System.Reflection.AmbiguousMatchException: Multiple custom attributes of the same type found
    GetCustomAttributes() возвращает перечисление атрибутов всех типов
    GetCustomAttributesData() возвращает перечисление CustomAttributeData, в котором есть свойства позволяющие получить конструктор, параметры (именованные и позиционные), аргументы конструктора
    IsDefined(Type attrType, bool inherit) возвращает true, если атрибут объявлен над элементом, false если нет

    Для наглядности предлагаю взглянуть на небольшое демо работы всех упомянутых функций.

    Возвращаемое значение упомянутых выше методов
    [AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
    public class LogAttribute : Attribute
    {
    public string LogName { get; set; }
    }
    
    [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
    public class SerializeAttribute : Attribute
    {
    public string SerializeName { get; set; }
    }
    
    [Log(LogName = "LogBase1")]
    [Log(LogName = "LogBase2")]
    [Serialize(SerializeName = "SerializeBase1")]
    [Serialize(SerializeName = "SerializeBase2")]
    public class RandomDomainEntityBase
    {
    [Log(LogName = "LogMethod1")]
    [Log(LogName = "LogMethod2")]
    [Serialize(SerializeName = "SerializeMethod1")]
    [Serialize(SerializeName = "SerializeMethod2")]
    public virtual void VirtualMethod()
    {
    throw new NotImplementedException();
    }
    }
    
    [Log(LogName = "LogDerived1")]
    [Log(LogName = "LogDerived2")]
    [Serialize(SerializeName = "SerializeDerived1")]
    [Serialize(SerializeName = "SerializeDerived2")]
    public class RandomDomainEntityDerived : RandomDomainEntityBase
    {
    [Log(LogName = "LogOverride1")]
    [Log(LogName = "LogOverride2")]
    [Serialize(SerializeName = "SerializeOverride1")]
    [Serialize(SerializeName = "SerializeOverride2")]
    public override void VirtualMethod()
    {
    throw new NotImplementedException();
    }
    }
    
    class Program
    {
    static void Main()
    {
    Type derivedType = typeof(RandomDomainEntityDerived);
    MemberInfo overrideMethod = derivedType.GetMethod("VirtualMethod");
    
    // overrideMethod.GetCustomAttribute(typeof(LogAttribute)); //System.Reflection.AmbiguousMatchException: Multiple custom attributes of the same type found.
    // overrideMethod.GetCustomAttribute<LogAttribute>(false); // System.Reflection.AmbiguousMatchException: Multiple custom attributes of the same type found.
    // Attribute.GetCustomAttribute(derivedType, typeof(SerializeAttribute)); // System.Reflection.AmbiguousMatchException: Multiple custom attributes of the same type found.
    
    IEnumerable<Attribute> allCustom = overrideMethod.GetCustomAttributes(); //LogOverride1 LogOverride2 SerializeOverride1 SerializeOverride2 LogMethod1 LogMethod2
    IList<CustomAttributeData> allCustomInfo = overrideMethod.GetCustomAttributesData(); // то же, но в другом виде
    IEnumerable<LogAttribute> typedCustom1 = overrideMethod.GetCustomAttributes<LogAttribute>(inherit:false); //LogOverride1 LogOverride2
    IEnumerable<LogAttribute> typedInheritedCustom1 = overrideMethod.GetCustomAttributes<LogAttribute>(inherit:true); //LogOverride1 LogOverride2 LogMethod1 LogMethod2
    IEnumerable<SerializeAttribute> typedCustom2 = overrideMethod.GetCustomAttributes<SerializeAttribute>(inherit:false); //SerializeOverride1 SerializeOverride2
    IEnumerable<SerializeAttribute> typedInheritedCustom2 = overrideMethod.GetCustomAttributes<SerializeAttribute>(inherit:true); //SerializeOverride1 SerializeOverride2
    Attribute[] customFromStaticClass = Attribute.GetCustomAttributes(overrideMethod, typeof(SerializeAttribute), inherit:true); //SerializeOverride1 SerializeOverride2
    
    IEnumerable<Attribute> classCustom = derivedType.GetCustomAttributes(); //LogDerived1 LogDerived2 SerializeDerived1 SerializeDerived2 LogBase1 LogBase2
    IList<CustomAttributeData> classCustomInfo = derivedType.GetCustomAttributesData(); // то же, но в другом виде
    IEnumerable<LogAttribute> typedClassCustom1 = derivedType.GetCustomAttributes<LogAttribute>(false); //LogDerived1 LogDerived2
    IEnumerable<LogAttribute> typedInheritedClassCustom1 = derivedType.GetCustomAttributes<LogAttribute>(true); //LogDerived1 LogDerived2 LogBase1 LogBase2
    IEnumerable<SerializeAttribute> typedClassCustom2 = derivedType.GetCustomAttributes<SerializeAttribute>(false); //SerializeDerived1 SerializeDerived2
    IEnumerable<SerializeAttribute> typedInheritedClassCustom2 = derivedType.GetCustomAttributes<SerializeAttribute>(true); //SerializeDerived1 SerializeDerived2
    }
    }
    


    Ну и для академического интереса привожу пример определения атрибутов во время выполнения. Данный код не претендует на звание самого красивого и поддерживаемого.

    Код
    public class TypeCreator
    {
    private const string TypeSignature = "DynamicType";
    private const string ModuleName = "DynamicModule";
    private const string AssemblyName = "DynamicModule";
    
    private readonly TypeBuilder _typeBuilder = GetTypeBuilder();
    
    public object CreateTypeInstance()
    {
        _typeBuilder.DefineNestedType("ClassName");
        CreatePropertyWithAttribute<SerializeAttribute>("PropWithAttr", typeof(int), new Type[0], new object[0]);
        Type newType = _typeBuilder.CreateType();
        return Activator.CreateInstance(newType);
    }
    
    private void CreatePropertyWithAttribute<T>(string propertyName, Type propertyType, Type[] ctorTypes, object[] ctorArgs) where T : Attribute
    {
        var attributeCtor = typeof(T).GetConstructor(ctorTypes);
        CustomAttributeBuilder caBuilder = new CustomAttributeBuilder(attributeCtor, ctorArgs);
        PropertyBuilder newProperty = CreateProperty(propertyName, propertyType);
        newProperty.SetCustomAttribute(caBuilder);
    }
    
    private PropertyBuilder CreateProperty(string propertyName, Type propertyType)
    {
        FieldBuilder fieldBuilder = _typeBuilder.DefineField(propertyName, propertyType, FieldAttributes.Private);
        PropertyBuilder propertyBuilder = _typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
        MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;
        MethodBuilder getter = GenerateGetter();
        MethodBuilder setter = GenerateSetter();
        propertyBuilder.SetGetMethod(getter);
        propertyBuilder.SetSetMethod(setter);
        return propertyBuilder;
    
        MethodBuilder GenerateGetter()
        {
            MethodBuilder getMethodBuilder = _typeBuilder.DefineMethod($"get_{propertyName}", getSetAttr, propertyType, Type.EmptyTypes);
            ILGenerator getterIl = getMethodBuilder.GetILGenerator();
            getterIl.Emit(OpCodes.Ldarg_0);
            getterIl.Emit(OpCodes.Ldfld, fieldBuilder);
            getterIl.Emit(OpCodes.Ret);
            return getMethodBuilder;
        }
    
        MethodBuilder GenerateSetter()
        {
            MethodBuilder setMethodBuilder = _typeBuilder.DefineMethod($"set_{propertyName}", getSetAttr, null,new [] { propertyType });
            ILGenerator setterIl = setMethodBuilder.GetILGenerator();
            setterIl.Emit(OpCodes.Ldarg_0);
            setterIl.Emit(OpCodes.Ldarg_1);
            setterIl.Emit(OpCodes.Stfld, fieldBuilder);
            setterIl.Emit(OpCodes.Ret);
            return setMethodBuilder;
        }
    }
    
    private static TypeBuilder GetTypeBuilder()
    {
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(AssemblyName), AssemblyBuilderAccess.Run);
        ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(ModuleName);
        TypeBuilder typeBuilder = moduleBuilder.DefineType(TypeSignature,
        TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass |
        TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout,null);
        return typeBuilder;
    }
    }
    
    public class Program
    {
    public static void Main()
    {
        object instance = new TypeCreator().CreateTypeInstance();
        IEnumerable<Attribute> attrs = instance.GetType().GetProperty("PropWithAttr").GetCustomAttributes(); // Serializable
    }
    }
    

    • +24
    • 8,4k
    • 1
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 1

      +2

      Для коллекции:
      MethodImplAttribute — с помощью него можно задать, например, инлайн метода.
      CallerMemberNameAttribute — весьма полезен при реализации INotifyPropertyChanged


      Вообще, рекомендую ознакомиться с остальными атрибутами в CompilerServices, могут оказаться полезными.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое