.NET Tutorial
Middle6 мин чтенияВопрос 14 из 25

Атрибуты: создание, чтение через Reflection, компилируемые атрибуты?

Как атрибуты используются в .NET

C#
[ApiController]                                        // ASP.NET Core: поведение контроллера
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    [HttpPost]                                         // маршрутизация
    [Authorize(Policy = "CanCreateOrders")]            // авторизация
    [ProducesResponseType(typeof(OrderDto), 201)]      // OpenAPI
    public IActionResult Create([FromBody] CreateOrderRequest request) => ...;
}

public class CreateOrderRequest
{
    [Required, StringLength(100)]                      // валидация
    public string CustomerName { get; init; } = "";

    [JsonPropertyName("total_amount")]                 // сериализация
    public decimal Total { get; init; }
}

[Table("orders")]                                      // EF Core: маппинг
public class Order { [Key] public int Id { get; set; } }

[Obsolete("Используйте CreateV2", error: false)]       // компилятор: предупреждение
public void Create() { }

[Obsolete], [CallerMemberName],
[Conditional]

анализаторы и
source generators

[Атрибут] в исходном коде

Компилятор Roslyn

Метаданные сборки
(таблица CustomAttribute)

Предупреждения,
подстановка значений

Сгенерированный код

Рантайм через Reflection:
ASP.NET Core, EF Core,
System.Text.Json, xUnit

CLR: [Serializable],
[StructLayout], [DllImport]

[Obsolete], [CallerMemberName],
[Conditional]

анализаторы и
source generators

[Атрибут] в исходном коде

Компилятор Roslyn

Метаданные сборки
(таблица CustomAttribute)

Предупреждения,
подстановка значений

Сгенерированный код

Рантайм через Reflection:
ASP.NET Core, EF Core,
System.Text.Json, xUnit

CLR: [Serializable],
[StructLayout], [DllImport]

Кто и когда читает атрибуты

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

C#
[AttributeUsage(
    AttributeTargets.Class | AttributeTargets.Method,  // где можно применять
    AllowMultiple = false,                             // можно ли указать несколько раз
    Inherited = true)]                                 // видят ли наследники
public sealed class AuditAttribute : Attribute
{
    public AuditAttribute(string action) => Action = action;   // позиционный параметр

    public string Action { get; }
    public bool IncludePayload { get; set; }                   // именованный параметр
}

[Audit("order.create", IncludePayload = true)]
public class CreateOrderHandler { }

Соглашения:

  • Имя класса заканчивается на Attribute, при использовании суффикс можно опустить.
  • Класс атрибута делают sealed — рефлексия ищет атрибуты быстрее.
  • Параметры конструктора — обязательные (позиционные), публичные свойства — необязательные (именованные).

Generic-атрибуты (C# 11)

C#
public sealed class ValidatorAttribute<TValidator> : Attribute where TValidator : IValidator { }

[Validator<CreateOrderValidator>]      // вместо [Validator(typeof(CreateOrderValidator))]
public record CreateOrderCommand(int CustomerId);

Чтение через Reflection

C#
// Один атрибут
var audit = typeof(CreateOrderHandler).GetCustomAttribute<AuditAttribute>();
if (audit is not null)
    Console.WriteLine($"{audit.Action}, payload: {audit.IncludePayload}");

// Проверка наличия (быстрее — объект атрибута не создаётся)
bool isAudited = typeof(CreateOrderHandler).IsDefined(typeof(AuditAttribute), inherit: true);

// Все свойства, помеченные атрибутом
var sensitive = typeof(User).GetProperties()
    .Where(p => p.IsDefined(typeof(SensitiveDataAttribute)))
    .Select(p => p.Name)
    .ToArray();

Практический пример: маскирование данных в логах

C#
[AttributeUsage(AttributeTargets.Property)]
public sealed class SensitiveAttribute : Attribute { }

public record LoginRequest(string Email, [property: Sensitive] string Password);

public static class Masker
{
    private static readonly ConcurrentDictionary<Type, PropertyInfo[]> Props = new();

    public static Dictionary<string, object?> ToSafeDictionary(object obj)
    {
        var props = Props.GetOrAdd(obj.GetType(), t => t.GetProperties());
        return props.ToDictionary(
            p => p.Name,
            p => p.IsDefined(typeof(SensitiveAttribute)) ? "***" : p.GetValue(obj));
    }
}

Обратите внимание на [property: Sensitive] — у positional record атрибут по умолчанию применяется к параметру конструктора, а не к свойству. Цель задают явно: property:, field:, param:, return:, assembly:.

Атрибуты для компилятора

Атрибут Что делает
[Obsolete] Предупреждение или ошибка при использовании
[CallerMemberName], [CallerFilePath], [CallerLineNumber] Компилятор подставляет имя вызывающего метода, путь, строку
[CallerArgumentExpression] (C# 10) Подставляет текст выражения-аргумента — для ArgumentNullException.ThrowIfNull
[Conditional("DEBUG")] Вызовы метода удаляются из сборки без символа DEBUG
[MethodImpl(MethodImplOptions.AggressiveInlining)] Подсказка JIT
[NotNull], [MaybeNullWhen(false)] Подсказки анализу nullable
[ModuleInitializer] Метод вызывается при загрузке сборки
[SetsRequiredMembers] Конструктор инициализирует все required-члены
C#
public static void NotNull(object? value, [CallerArgumentExpression(nameof(value))] string? expr = null)
{
    if (value is null) throw new ArgumentNullException(expr);
}

NotNull(request.Customer);   // ArgumentNullException: request.Customer

Атрибуты и Source Generators

Современный подход: атрибут служит маркером для генератора, а логика появляется на этапе компиляции — без рефлексии в рантайме. Примеры: [LoggerMessage], [GeneratedRegex], [JsonSerializable], [LibraryImport], сторонние — [AutoConstructor], Mapperly [Mapper].

Вопросы на засыпку

Может ли атрибут содержать логику?

Может содержать методы, но вызывает их тот, кто читает атрибут. Например, ValidationAttribute.IsValid вызывает валидатор, ActionFilterAttribute.OnActionExecuting — конвейер MVC. Сам по себе атрибут пассивен.

Почему в атрибут нельзя передать decimal или DateTime?

Аргументы хранятся в метаданных сборки в специальном бинарном формате, который поддерживает только примитивы, строки, Type, enum и массивы из них. decimal и DateTime — обычные структуры, для них формата нет. Обходной путь — передать строку и распарсить.

Чем атрибуты отличаются от маркерных интерфейсов?

Интерфейс участвует в системе типов: можно ограничить дженерик where T : IMarker, проверить is IMarker быстро и без рефлексии. Атрибут может нести параметры и применяться к методам, свойствам, параметрам — не только к типам.

Как фильтр-атрибут в ASP.NET Core получает зависимости из DI?

Конструктор атрибута не может принимать сервисы. Используют [ServiceFilter(typeof(MyFilter))] или [TypeFilter(typeof(MyFilter))] — фильтр создаётся через DI, а атрибут лишь указывает тип.

Итог

Атрибуты — метаданные, которые читают компилятор, рантайм и фреймворки. Создавайте свои через наследование от Attribute с [AttributeUsage], читайте через рефлексию с кэшированием, а для производительного кода используйте атрибуты как маркеры для source generators.