Метод программирования, именуемый аспектно-ориентированным, впервые явился миру в конце девяностых годов прошлого века, когда группа исследователей из Xerox PARC под руководством Грегора Кичалеса решила, что объектно-ориентированного подхода человечеству недостаточно. Они создали AspectJ — расширение для Java, призванное разрешить проблему, которую окрестили «сквозной функциональностью». Суть проблемы проста до безобразия: код логирования, обработки ошибок, проверки прав доступа и прочих служебных радостей размазывается по всему приложению, как масло по по́лу, превращая элегантную бизнес-логику в свалку повторяющихся конструкций.

Аспектно-ориентированное программирование предлагает выделить эти сквозные concerns в отдельные сущности — аспекты, которые можно применять к коду декларативно, не засоряя основную логику техническими деталями. В теории звучит как серебряная пуля. На практике AspectJ оказался инструментом, требующим от программиста понимания магических pointcut expressions и готовности смириться с тем, что код компилируется через специальный компилятор, производящий байткод, который отладить можно только с поллитрой, бубном или молитвенником.

Весь этот балаган приключился лет тридцать тому назад. С тех пор аспектно-ориентированное программирование обрело статус «той штуки, о которой все слышали, но никто не использует», заняв почётное место рядом с CORBA и XML-конфигами на три тысячи строк. Причин тому множество: сложность инструментария, непрозрачность происходящего, отсутствие нормальной поддержки в IDE, и главное — ощущение, что ты буквально продал душу дьяволу ради избавления от десятка строк boilerplate-кода.

Недавно ради старого знакомого я вписался в аудит восьмилетнего финтеховского проекта на дотнете — и, ну как бы это помягче… В общем, метрики они не собирали. Аудит сотен тысяч строк кода на неродном языке без visibility — удовольствие то еще, да и я привык отвечать за результат в продакшене, а не в мертвой ветке гитхаба. Короче, я начал с телеметрии; если бы я расставлял вызовы ручками — я бы отъехал в дурку на третий день. Поэтому я познакомился с Metalama — библиотекой, которая обещает вернуть аспектно-ориентированному программированию утраченную прелесть (я был фанатом AOP еще двадцать с лишним лет назад, я даже в руби его затащил). Создатели Metalama, видимо, внимательно изучили все ошибки предшественников и решили сделать AOP таким, каким он должен был быть с самого начала: понятным, прозрачным и работающим непосредственно с исходным кодом C#, а не с какими-то абстрактными байткодами или IL-инструкциями.

Metalama работает на этапе компиляции, генерируя C#-код, который можно увидеть, отладить и понять без степени в компьютерной магии. Библиотека интегрируется с Roslyn — и предоставляет API для создания аспектов, которые модифицируют код во время сборки. Ключевое отличие от AspectJ и прочих динозавров: всё происходит в терминах C#, без изобретения собственных DSL и синтаксических конструкций, понятных только посвящённым.

Вот вам несколько примеров.

Логирование (телеметрия устроена так же)

Классический подход — скопировать строчку с вызовом логгера в начало каждого метода, а затем провести остаток дня, проклиная бренность бытия и отсутствие метакопипаста. Вот как это выглядит без аспектов:

public class OrderService
{
    private readonly ILogger _logger;

    public void CreateOrder(Order order)
    {
        _logger.LogInformation("CreateOrder called with {OrderId}", order.Id);
        // Бизнес-логика создания заказа
        _logger.LogInformation("CreateOrder completed");
    }

    public void CancelOrder(string orderId)
    {
        _logger.LogInformation("CancelOrder called with {OrderId}", orderId);
        // Бизнес-логика отмены заказа
        _logger.LogInformation("CancelOrder completed");
    }

    // И так далее для каждого из ста методов...
}

С Metalama вы создаёте аспект LogAttribute, помечаете им нужные методы или целые классы, и библиотека автоматически добавляет код логирования при компиляции:

public class LogAttribute : OverrideMethodAspect
{
    public override dynamic? OverrideMethod()
    {
        var methodName = meta.Target.Method.Name;
        var parameters = meta.Target.Parameters.ToArray();
        
        Console.WriteLine($"Entering {methodName}");
        
        try
        {
            var result = meta.Proceed();
            Console.WriteLine($"Leaving {methodName} successfully");
            return result;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Exception in {methodName}: {ex.Message}");
            throw;
        }
    }
}

Теперь применяем аспект:

[Log]
public class OrderService
{
    public void CreateOrder(Order order)
    {
        // Только бизнес-логика, без захламления логированием
    }

    public void CancelOrder(string orderId)
    {
        // То же самое — чистый код (в смысле, без артефактов логирования)
    }
}

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

INotifyPropertyChanged: бич WPF-разработчиков

Другой классический пример — реализация INotifyPropertyChanged для UI-приложений. Каждый, кто писал на WPF или Xamarin, знает эту боль. Без аспектов каждое свойство превращается в трёхстрочную конструкцию:

public class PersonViewModel : INotifyPropertyChanged
{
    private string _firstName;
    public string FirstName
    {
        get => _firstName;
        set
        {
            if (_firstName != value)
            {
                _firstName = value;
                OnPropertyChanged(nameof(FirstName));
            }
        }
    }

    private string _lastName;
    public string LastName
    {
        get => _lastName;
        set
        {
            if (_lastName != value)
            {
                _lastName = value;
                OnPropertyChanged(nameof(LastName));
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    // И ещё двадцать восемь свойств...
}

С Metalama достаточно пометить класс атрибутом, и весь boilerplate генерируется автоматически:

[NotifyPropertyChanged]
public class PersonViewModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    // Все свойства — автоматические, без backing fields и OnPropertyChanged
}

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

Валидация параметров: defensive programming без занудства

Проверка входных параметров — ещё один источник бесконечного copy-paste. Классический подход выглядит так:

public class UserService
{
    public void RegisterUser(string email, string password, int age)
    {
        if (string.IsNullOrEmpty(email))
            throw new ArgumentNullException(nameof(email));
        if (string.IsNullOrEmpty(password))
            throw new ArgumentNullException(nameof(password));
        if (age < 0)
            throw new ArgumentOutOfRangeException(nameof(age));
            
        // Собственно логика регистрации
    }

    public void UpdateProfile(string userId, string bio)
    {
        if (string.IsNullOrEmpty(userId))
            throw new ArgumentNullException(nameof(userId));
        if (string.IsNullOrEmpty(bio))
            throw new ArgumentNullException(nameof(bio));
            
        // Логика обновления профиля
    }
}

С Metalama создаём аспект для валидации:

public class NotNullAttribute : ContractAspect
{
    public override void Validate(dynamic? value)
    {
        if (value == null || (value is string str && string.IsNullOrEmpty(str)))
        {
            var parameterName = meta.Target.Parameter.Name;
            throw new ArgumentNullException(parameterName);
        }
    }
}

Применяем:

public class UserService
{
    public void RegisterUser(
        [NotNull] string email, 
        [NotNull] string password, 
        [Range(0, 150)] int age)
    {
        // Только бизнес-логика
    }

    public void UpdateProfile([NotNull] string userId, [NotNull] string bio)
    {
        // Никаких проверок вручную
    }
}

Архитектурная валидация: enforcement на стероидах

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

Например, хотите убедиться, что все публичные методы в контроллерах помечены атрибутами авторизации:

public class RequireAuthorizationAttribute : TypeAspect
{
    public override void BuildAspect(IAspectBuilder<INamedType> builder)
    {
        foreach (var method in builder.Target.Methods.Where(m => m.Accessibility == Accessibility.Public))
        {
            if (!method.Attributes.Any(a => a.Type.Name.Contains("Authorize")))
            {
                builder.Diagnostics.Report(
                    Diagnostic.Create(
                        "ARCH001",
                        "Public controller method must have authorization attribute",
                        method));
            }
        }
    }
}

Применяем к базовому контроллеру:

[RequireAuthorization]
public abstract class BaseController : ControllerBase
{
}

public class UserController : BaseController
{
    // Ошибка компиляции: метод без [Authorize]
    public IActionResult GetUsers() => Ok();
    
    // Всё в порядке
    [Authorize]
    public IActionResult DeleteUser(string id) => Ok();
}

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

Кэширование: ещё один пример

Добавление кэширования к методам — типичная задача, которая обычно требует обёртывания каждого метода в конструкцию проверки кэша:

public class CacheAttribute : OverrideMethodAspect
{
    public override dynamic? OverrideMethod()
    {
        var cacheKey = $"{meta.Target.Type.Name}.{meta.Target.Method.Name}";
        
        if (Cache.TryGet(cacheKey, out var cachedResult))
        {
            Console.WriteLine($"Cache hit for {cacheKey}");
            return cachedResult;
        }
        
        var result = meta.Proceed();
        Cache.Set(cacheKey, result, TimeSpan.FromMinutes(5));
        
        return result;
    }
}

Применяем:

public class ProductService
{
    [Cache]
    public List<Product> GetProducts()
    {
        // Дорогостоящая операция загрузки из базы
        return _repository.LoadAllProducts();
    }
}

Реальность использования

Библиотека поддерживает интеграцию с IDE, предоставляя немедленную обратную связь. Изменили аспект — увидели результат в редакторе, не перекомпилируя проект. Это кардинально меняет опыт разработки: вместо цикла «скопипастил-скомпилировал-ужаснулся-крякнул-отменил» (в просторечии: «ССУКО») — получаешь мгновенную визуализацию того, что аспект делает с кодом. Metalama даже предоставляет возможность просматривать сгенерированный код прямо в IDE, что превращает отладку из шаманского ритуала в обычную инженерную задачу. Да, даже в моём виме.

Конечно, в этом мире живут не только радужные пони и веселые единороги. Метапрограммирование всегда несёт риск создания кода, который сложно понять человеку, впервые открывшему проект. Аспект может модифицировать код неочевидным образом, и новый разработчик потратит час, разбираясь, откуда в методе взялась строчка, которой нет в исходнике. Это плата за избавление от boilerplate, и платить её придётся документацией, именованием и архитектурной дисциплиной. Metalama пытается минимизировать этот риск прозрачностью — сгенерированный код доступен для просмотра, аспекты пишутся на обычном C#, но фундаментальную проблему это не решает: магия остаётся магией, даже если она документирована и предсказуема.

Ещё один момент: Metalama работает на этапе компиляции, что означает увеличение времени сборки. Для небольших проектов это незаметно, но в крупных энтерпрайзных монстрах каждая секунда компиляции превращается в вечность, особенно когда вы в сотый раз за день пересобираете проект после правки опечатки. Создатели библиотеки оптимизируют производительность, но законы физики обмануть сложно: метапрограммирование требует анализа и генерации кода, сиречь — времени.

Best practices

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

Хорошие практики при использовании Metalama сводятся (как всё и как всегда, в принципе) к здравому смыслу: не злоупотребляйте аспектами там, где достаточно обычного наследования или композиции; документируйте аспекты подробно, объясняя, что они делают и почему; держите аспекты простыми и фокусируйте каждый на одной задаче; используйте валидацию для проверки архитектурных инвариантов; и главное — помните, что аспекты это инструмент, а не религия. Иногда три строки повторяющегося кода лучше, чем аспект, который нужно поддерживать и объяснять каждому новому члену команды.


Аспектно-ориентированное программирование прошло долгий путь от академических экспериментов Xerox PARC до практичного инструмента для промышленной разработки. Metalama демонстрирует, что идеи, заложенные в AspectJ четверть века назад, могут работать, если подойти к их реализации прагматично, без фанатизма и с уважением к разработчикам, которым потом этот код поддерживать. Библиотека не спасёт мир и не превратит плохого программиста в хорошего, но она может сделать жизнь разработчика чуть менее монотонной, избавив от необходимости в тысячный раз писать одну и ту же конструкцию. А это уже немало.