[Из песочницы] Aspect Oriented Programming (AOP) через исходный код

pw2fw6fhcagoqxtrjpf5-lhjveg.png

Аспектно-ориентированное программирование очень привлекательная концепция для упрощения кодовой базы, создания чистого кода и минимизации ошибок типа копипасты.

Сегодня, в большинстве случаев, внедрение аспектов идёт на уровне байт кода, т.е. после компиляции, некий инструмент «вплетает» дополнительный байт код с поддержкой требуемой логики.

Наш подход (также как и подход некоторых других инструментов), это модификация исходного кода для внедрения логики аспектов. С переходом на технологию Roslyn, добиться этого очень легко и результат даёт определённые преимущества по сравнению с модификацией непосредственно байт кода.

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

Особенно в средне-крупных проектах корпоративного уровня, где формализованны требования к функциональности продуктов. Для примера, может существовать требование — при установки флага конфигурации осуществить логирование всех входных параметров для всех публичных методов. Или для всех методов проекта иметь систему нотификации которая пошлёт сообщение при превышении некого порога времени исполнения этого метода.

Как это делается без AOP? Или забивается и делается только для наиболее важных частей или при написании новых методов идёт копипаста подобного кода из соседних методов, со всеми сопутствующими подобного способа.

При использовании AOP, один раз пишется advice который применяется к проекту и дело сделано. Когда надо будет немного обновить логику, вы опять же один раз обновите advice и он будет применён при следующей сборке. Без AOP, 100500 обновлений по всему коду проекта.

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

После внедрения AOP в ваш проект, вы начинаете внедрять вещи о которых без него даже не мечтали, потому что они выглядили как отностительно малое преимущество, при больших затратах. При AOP всё с точностью наоборот, относительно малые затраты и большие преимущества (для подобного уровня затрат ваших усилий).

По моим ощущениям, в экосистеме .Net, аспектно-ориентированное программирование существенно менее популярно в сравнении с экосистемой Java. Я думаю, что основная причина это отсутствие бесплатного и открытого инструментария, сравнимого с функциональностью и качеством такого в Java.

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

Можно сравнить возможности инструментов (надо иметь в виду что сравнение сделано владельцем PostSharp, но некоторую картину оно даёт).

Наш путь к аспектно-ориентированному программированию


Мы небольшая консалтинговая компания (12 человек) и конечным результатом нашей работы является исходный код. Т.е. нам платят за то что мы создаём исходный код, качественный код. Мы работаем только в одной индустрии и многие наши проекты имеют очень похожие требования и как результат, исходный код также достаточно похож между этими проектам.

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

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

Следующим логическим шагом, была идея внедрить использование аспектно-ориентированного программирования. Мы попробовали несколько подходов и инструментов, но результат был далёк от наших ожиданий. По времени это совпало с выходом технологии Roslyn и в определённый момент у нас родилась идея объединить возможности автоматической генерации кода и Roslyn.

Буквально за пару недель был создан прототип инструмента и по нашим ощущениям, данный подход показался более перспективным. Спустя несколько итераций в способах использования и обновлений этого инструмента, можно сказать что наши ожидания оправдались и даже более чем мы того рассчитывали. Мы наработали библиотеку полезных шаблонов и используем данный подход в большинстве наших проектов и некоторые наши клиенты также его используют и даже заказывают разработку шаблонов под свои нужды.

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

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

Как это было бы сделано в идеальном мире


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

В моём виденьи идеального мира, спецификации языка позволяют использование трансформации исходного кода и существует поддержка компилятором и IDE.

Идея была навеяна включением модификатора «partial» в спецификации языка C#. Эта, достаточно простая концепция (возможность определения класса, структуры или интерфейса в нескольких файлах) кардинально улучшила и упростила поддержку инструментов для автоматической генерации исходного код. Т.е. это своего рода горизонтальная разбивка исходного кода класса между несколькими файлами. Для тех кто не знает языка C#, небольшой пример.

Предположим у нас есть простая форма описанная в файле Example1.aspx

<%@ Page Language="C#" AutoEventWireup="True" %>
// . . .

// . . .


И пользовательская логика (например изменение цвета кнопки на красный при её нажатии) в файле Example1.aspx.cs

public partial class ExamplePage1 : System.Web.UI.Page, IMyInterface
{
  protected void btnSubmit_Click(Object sender, EventArgs e) 
  {
    btnSubmit.Color = Color.Red;
  }
}


Наличие в языке возможностей предоставляемых «partial» позволяет инструментарию распарсить файл Example1.aspx и автоматически сгенерировать файл Example1.aspx.designer.cs

public partial class ExamplePage1 : System.Web.UI.Page
{
  protected global::System.Web.UI.WebControls.Button btnSubmit;
}


Т.е. мы имеем возможность хранить часть кода для класса ExamplePage1 в одном файле обновляемым программистом (Example1.aspx.cs) и часть в файле Example1.aspx.designer.cs автоматически генерируемым инструментарием. Для компилятора же это выглядит в конце концов как один общий класс

public class ExamplePage1 : System.Web.UI.Page, IMyInterface
{ 
  protected global::System.Web.UI.WebControls.Button btnSubmit;

  protected void btnSubmit_Click(Object sender, EventArgs e) 
  {
    btnSubmit.Color = Color.Red;
  }
}


На примере с определением наследования интерфейса IMyInterface, видно, что окончательный результат это комбинирование определений класса из разных файлов.

Если у нас отсутствует функциональность подобная partial и компилятор требует хранение всего кода класса только в одном файле, то можно предположить неудобства и дополнительные телодвижения необходимые для поддержки автогенерации.

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

Первый модификатор это original и его добавляем в определение класса который должен иметь возможность быть трансформирован.

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

Последовательность примерно такая

  1. Пользователь работает с исходным кодом класса который содержит модификатор original в файле .cs (например Example1.cs)
  2. При компиляции, компилятор проверяет корректность исходного кода и если класс успешно скомпилировался, идёт проверка на наличие original
  3. Если original присутствует, то компилятор отдаёт исходный код этого файла процессу трансформации (который является чёрным ящиком для компилятора).
  4. Процесс трансформации базируясь на наборе правил выполняет модифицирование исходного код и при успешном завершении процесса создаёт файлы файла .processed.cs и файла .processed.cs.map (для соответствия кода между файлами .cs и файла .processed.cs, для помощи при отладки и для корректного отображения в IDE)
  5. Компилятор получает код из файла .processed.cs (в нашем примере это Example1.processed.cs) и компилирует уже этот код.
  6. Если код в файле успешно скомпилировался, то идёт проверка что

    a. Классы которые имели модификатор original имеют модификатор processed
    b. Сигнатура этих классов идентична как в файле .cs так и в файле .processed.cs

  7. Если всё нормально, то байт код полученный при компиляции файла .processed.cs включается в объектный файл для дальнейшего использования.


Т.е. добавив эти два модификатора, мы смогли на уровне языка организовать поддержку инструментов трансформации исходного кода, подобно тому как partial позволил упростить поддержку генерации исходного кода. Т.е. parial это горизонтальное разбитие кода, original/processed вертикальное.

Как мне видится, реализовать поддержку original/processed в компиляторе это неделя работы для двух интернов в компании Микрософт (шутка конечно, но она не далека от истины). По большому счёту, в этой задаче нету никаких фундаментальных сложностей, с точки зрения компилятора это манипуляция файлами и вызов процесса.

В .NET 5 была добавлена новая фича — генераторы исходного кода которая уже позволяет генерировать новые файлы исходного кода в процессе компиляции и это движение в правильном направлении. К сожалению она позволяет генерировать только новый исходный код, но не изменять существующий. Так что всё ещё ждём.

Пример подобного процесса. Пользователь создаёт файл Example2.cs

public original class ExamplePage2 : System.Web.UI.Page, IMyInterface
{ 
  protected global::System.Web.UI.WebControls.Button btnSubmit;

  protected void btnSubmit_Click(Object sender, EventArgs e) 
  {
    btnSubmit.Color = Color.Red;
  }	
}


Запускает на компиляцию, если всё прошло ок и компилятор видит модификатор original, то отдаёт исходный код, процессу трансформации, который генерирует файл Example2.processed.cs (в самом простейшем случае это может быть просто точная копия Example2.cs с заменённым original на processed).

В нашем случае мы предположим, что процесс трансформации добавил аспект логирования и результат выглядит как:

public processed class ExamplePage2 : System.Web.UI.Page, IMyInterface
{ 
  protected global::System.Web.UI.WebControls.Button btnSubmit;

  protected void btnSubmit_Click(Object sender, EventArgs e) 
  {
    try
    {
      btnSubmit.Color = Color.Red;
    } 
    catch(Exception ex)
    {
      ErrorLog(ex);
      throw;
    }

    SuccessLog();
  }	

  private static processed ErrorLog(Exception ex)
  {
    // some error logic here
  }

  private static processed SuccessLog([System.Runtime.CompilerServices.CallerMemberName] string memberName = "")
  {
    // some success logic here
  }
}


Следующий шаг, это проверка сигнатур. _Основные_ сигнатуры идентичны и удовлетворяют условию что определения в original и processed должны быть абсолютно одинаковы.

В этот пример я специально добавил ещё одно небольшое предложение, это модификатор processed для методов, свойств и полей.

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

Компилятор скомпилировал этот код и если всё ок, то взял байт код для продолжения процесса.

Понятно, что в данном примере идёт некоторое упрощение и в реальности логика может быть сложнее (например когда мы включаем оба original и partial для одного класса), но это не непреодолимая сложность.

Основная функциональность IDE в идеальном мире


Поддержка работы с исходным кодом файлов .processed.cs в IDE заключается в основном в корректной навигации между original/processed классами и переходов при пошаговой отладки.

Вторая по важности функция IDE (с моей точки зрения) это помощь в чтении кода processed классов. Processed класс может содержать множество частей кода, которые были добавлены несколькими аспектами. Реализация отображения которая похожа на концепцию слоёв в графическом редакторе представляется нам самым удобным вариантом для достижения данной цели. Наш текущей плагин реализует нечто подобное и реакция его пользователей вполне положительна.

Ещё одна функция которая бы сделала помогла внедрению AOP в повседневную жизнь, это функциональность refactoring, т.е. пользователь выделив часть кода мог бы сказать «Extract To AOP Template» и IDE создала правильные файлы, сгенерировала первоначальный код и проанализировав код проекта предложила кандидатов на использование шаблона из других классов.

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

Я уверен, что если за делов возьмутся создатели решарпера, то магия обеспечена.

Написание кода аспекта в идеальном мире


Если перефразировать ТРИЗ, то идеальное написание кода для реализации аспектов, это отсутствие написания дополнительного кода, который существует только для поддержки процессов инструментария.

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

Второе желание, это возможность иметь интерактивный plug&play, т.е. написав шаблон, нам бы не требовалось совершать дополнительные шаги, для того чтобы он мог быть использован для трансформации. Не требовалось перекомпилировать инструмент, отлавливать его ошибки и т.п. А также настраивать опции в проектах для посткомпиляции.

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

Ну и чтобы синтаксис шаблона был как можно ближе к синтаксису языка С#, в идеальном случае минорная надстройка, плюс несколько ключевых слов и плейсхолдеров.

Наша текущая реализация


К сожалению мы живём не в идеальном мире, поэтому приходится изобретать велосипеды и ездить на них.

Внедрение кода, компиляция и отладка


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

Сценарий примерно такой

  • Программист работает с оригинальной версией кода, реализует логику, компилирует проект чтобы обнаружить ошибки времени компиляции и т.п.
  • Когда он удовлетворён результатом и готов протестировать результаты своего труда, то запускается скрипт командной строки, который запускает процесс трансформации файлов и при успешной завершении трансформации, запускает процесс сборки.
  • После завершения сборки в зависимости от типа проекта запускается либо браузер, который открывает тестовый веб сайт для веб проекта, либо десктоп программа, если это WPF проект, либо автотесты и т.п.


Для отладки запускается вторая копия IDE, открывается странсформированная копия проекта и он работает с копией к которой была применена трансформация.

Процесс требует определённой дисциплины, но со времен приходит привычка и в определённых случаях подобный подход имеет некоторые преимущества (например билд можно запускать и деплоить на удалённый сервер, вместо того чтобы работать с локальной машиной). Плюс помощь со стороны плагина в VisualStudio упрощает процесс.

IDE


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

Например функциональность по отображению слоёв, в стиле подобному графическому редактору позволяет например скрывать/показывать слои комментариев, по области видимости (например чтобы были видены только публичные методы), регионам. Внедренный код обрамляется комментариями специального формата и они также могут быть скрыты как отдельный слой.

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

Также плагин предупреждает при попытке внесения изменений в странсформированную копию (чтобы не потерять их при последующей перетрансформации)

Конфигурация


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

Мы используем несколько уровней.

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

Второй уровень это указание на применение правил трансформации на уровне атрибутов классов, методов или полей.

Третий на уровне блока кода и четвёртый это явное указание на включение результатов трансформации шаблона в конкретное место в исходном коде.

Шаблоны


Исторически сложилось что для целей автоматической генерации мы используем шаблоны в формате T4, поэтому вполне логично было использовать этот же подход и в качестве шаблонов для трансформации. Шаблоны T4 включают в себя возможность исполнять произвольный код на C#, имеют минимальный оверхэд и хорошую выразительность.

Для тех кто никогда не работал с T4, самым простым аналогом будет представить ASPX формат, который вместо HTML генерирует исходный код на C# и исполняется не на IIS, а отдельной утилитой с выводом результата на консоль (или в файл).

Примеры


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

Исходный код примера перед трансформацией
// ##aspect=AutoComment

using AOP.Common;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Aspectimum.Demo.Lib
{

    [AopTemplate("ClassLevelTemplateForMethods", NameFilter = "First")]
    [AopTemplate("StaticAnalyzer", Action = AopTemplateAction.Classes)]
    [AopTemplate("DependencyInjection", AdvicePriority = 500, Action = AopTemplateAction.PostProcessingClasses)]
    [AopTemplate("ResourceReplacer", AdvicePriority = 1000, ExtraTag = "ResourceFile=Demo.resx,ResourceClass=Demo", Action = AopTemplateAction.PostProcessingClasses)]
    public class ConsoleDemo
    {
        public virtual Person FirstDemo(string firstName, string lastName, int age)
        {
            Console.Out.WriteLine("FirstDemo: 1");

            // ##aspect="FirstDemoComment" extra data here

            return new Person()
            {
                FirstName = firstName,
                LastName = lastName,
                Age = age,
            };
        }

        private static IConfigurationRoot _configuration = inject;
        private IDataService _service { get; } = inject;
        private Person _somePerson = inject;

        [AopTemplate("LogExceptionMethod")]
        [AopTemplate("StopWatchMethod")]
        [AopTemplate("MethodFinallyDemo", AdvicePriority = 100)]
        public Customer[] SecondDemo(Person[] people)
        {
            IEnumerable Customers;

            Console.Out.WriteLine("SecondDemo: 1");

            Console.Out.WriteLine(i18("SecondDemo: i18"));

            int configDelayMS = inject;
            string configServerName = inject;

            using (new AopTemplate("SecondDemoUsing", extraTag: "test extra"))
            {

                Customers = people.Select(s => new Customer()
                {
                    FirstName = s.FirstName,
                    LastName = s.LastName,
                    Age = s.Age,
                    Id = s.Id
                });

                _service.Init(Customers);

                foreach (var customer in Customers)
                {
                    Console.Out.WriteLine(i18($"First Name {customer.FirstName} Last Name {customer.LastName}"));
                    Console.Out.WriteLine("SecondDemo: 2 " + i18("First Name ") + customer.FirstName + i18(" Last Name   ") + customer.LastName);
                }
            }

            Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));
            Console.Out.WriteLine($"Server {configServerName} default delay {configDelayMS}");
            Console.Out.WriteLine($"Customer for ID=5 is {_service.GetCustomerName(5)}");

            return Customers.ToArray();
        }

        protected static string i18(string s) => s;
        protected static dynamic inject;

        [AopTemplate("NotifyPropertyChangedClass", Action = AopTemplateAction.Classes)]
        [AopTemplate("NotifyPropertyChanged", Action = AopTemplateAction.Properties)]
        public class Person
        {
            [AopTemplate("CacheProperty", extraTag: "{ \"CacheKey\": \"name_of_cache_key\", \"ExpiresInMinutes\": 10 }")]
            public string FullName
            {
                get
                {
                    // ##aspect="FullNameComment" extra data here
                    return $"{FirstName} {LastName}";
                }
            }

            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public int Age { get; set; }
        }

        [AopTemplate("NotifyPropertyChanged", Action = AopTemplateAction.Properties)]
        public class Customer : Person
        {
            public double CreditScore { get; set; }
        }

        public interface IDataService
        {
            void Init(IEnumerable customers);
            string GetCustomerName(int customerId);
        }

        public class DataService: IDataService
        {
            private IEnumerable _customers;
            public void Init(IEnumerable customers)
            {
                _customers = customers;
            }

            public string GetCustomerName(int customerId)
            {
                return _customers.FirstOrDefault(w => w.Id == customerId)?.FullName;
            }
        }

        public class MockDataService : IDataService
        {
            private IEnumerable _customers;
            public void Init(IEnumerable customers)
            {
                if(customers == null)
                    throw (new Exception("IDataService.Init(customers == null)"));
            }

            public string GetCustomerName(int customerId)
            {
                if (customerId < 0)
                    throw (new Exception("IDataService.GetCustomerName: customerId cannot be negative"));

                if (customerId == 0)
                    throw (new Exception("IDataService.GetCustomerName: customerId cannot be zero"));

                return $"FirstName{customerId} LastName{customerId}";
            }
        }
    }
}


Полная версия исходного кода после трансформации
//------------------------------------------------------------------------------
//  
//     This code was generated from a template.
// 
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
//
//  Generated base on file: ConsoleDemo.cs
//  ##sha256: ekmmxFSeH5ev8Epvl7QvDL+D77DHwq1gHDnCxzeBWcw
//  Created By: JohnSmith
//  Created Machine: 127.0.0.1
//  Created At: 2020-09-19T23:18:07.2061273-04:00
//
// 
//------------------------------------------------------------------------------
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;

namespace Aspectimum.Demo.Lib
{
    public class ConsoleDemo
    {
        public virtual Person FirstDemo(string firstName, string lastName, int age)
        {
            Console.Out.WriteLine("FirstDemo: 1");
            // FirstDemoComment replacement extra data here
            return new Person()
            {FirstName = firstName, LastName = lastName, Age = age, };
        }

        private static IConfigurationRoot _configuration = new ConfigurationBuilder()
            .SetBasePath(System.IO.Path.Combine(AppContext.BaseDirectory))
            .AddJsonFile("appsettings.json", optional: true)
            .Build();
        
        private IDataService _service { get; } = new DataService();

#error Cannot find injection rule for Person _somePerson
        private Person _somePerson = inject;

        public Customer[] SecondDemo(Person[] people)
        {
            try
            {
#error variable "Customers" doesn't match code standard rules
                IEnumerable Customers;
                
                Console.Out.WriteLine("SecondDemo: 1");

#error Cannot find resource for a string "SecondDemo: i18", please add it to resources
                Console.Out.WriteLine(i18("SecondDemo: i18"));

                int configDelayMS = Int32.Parse(_configuration["delay_ms"]);
                string configServerName = _configuration["server_name"];
                {
                    // second demo test extra
                    {
                        Customers = people.Select(s => new Customer()
                        {FirstName = s.FirstName, LastName = s.LastName, Age = s.Age, Id = s.Id});
                        _service.Init(Customers);
                        foreach (var customer in Customers)
                        {
                            Console.Out.WriteLine(String.Format(Demo.First_Last_Names_Formatted, customer.FirstName, customer.LastName));
                            Console.Out.WriteLine("SecondDemo: 2 " + (Demo.First_Name + " ") + customer.FirstName + (" " + Demo.Last_Name + "   ") + customer.LastName);
                        }
                    }
                }

#error Argument for i18 method must be either string literal or interpolated string, but instead got Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax
#warning Please replace String.Format with string interpolation format.
                Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));
                Console.Out.WriteLine($"Server {configServerName} default delay {configDelayMS}");
                Console.Out.WriteLine($"Customer for ID=5 is {_service.GetCustomerName(5)}");

                return Customers.ToArray();
            }
            catch (Exception logExpn)
            {
                Console.Error.WriteLine($"Exception in SecondDemo\r\n{logExpn.Message}\r\n{logExpn.StackTrace}");
                throw;
            }
        }

        protected static string i18(string s) => s;
        protected static dynamic inject;
        public class Person : System.ComponentModel.INotifyPropertyChanged
        {
            public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
            protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
            {
                PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
            }

            public string FullName
            {
                get
                {
                    System.Runtime.Caching.ObjectCache cache = System.Runtime.Caching.MemoryCache.Default;
                    string cachedData = cache["name_of_cache_key"] as string;
                    if (cachedData == null)
                    {
                        cachedData = GetPropertyData();
                        if (cachedData != null)
                        {
                            cache.Set("name_of_cache_key", cachedData, System.DateTimeOffset.Now.AddMinutes(10));
                        }
                    }

                    return cachedData;
                    string GetPropertyData()
                    {
                        // FullNameComment FullName
                        return $"{FirstName} {LastName}";
                    }
                }
            }

            private int _id;
            public int Id
            {
                get
                {
                    return _id;
                }

                set
                {
                    if (_id != value)
                    {
                        _id = value;
                        NotifyPropertyChanged();
                    }
                }
            }

            private string _firstName;
            public string FirstName
            {
                get
                {
                    return _firstName;
                }

                set
                {
                    if (_firstName != value)
                    {
                        _firstName = value;
                        NotifyPropertyChanged();
                    }
                }
            }

            private string _lastName;
            public string LastName
            {
                get
                {
                    return _lastName;
                }

                set
                {
                    if (_lastName != value)
                    {
                        _lastName = value;
                        NotifyPropertyChanged();
                    }
                }
            }

            private int _age;
            public int Age
            {
                get
                {
                    return _age;
                }

                set
                {
                    if (_age != value)
                    {
                        _age = value;
                        NotifyPropertyChanged();
                    }
                }
            }
        }

        public class Customer : Person
        {
            private double _creditScore;
            public double CreditScore
            {
                get
                {
                    return _creditScore;
                }

                set
                {
                    if (_creditScore != value)
                    {
                        _creditScore = value;
                        NotifyPropertyChanged();
                    }
                }
            }
        }

        public interface IDataService
        {
            void Init(IEnumerable customers);
            string GetCustomerName(int customerId);
        }

        public class DataService : IDataService
        {
            private IEnumerable _customers;
            public void Init(IEnumerable customers)
            {
                _customers = customers;
            }

            public string GetCustomerName(int customerId)
            {
                return _customers.FirstOrDefault(w => w.Id == customerId)?.FullName;
            }
        }

        public class MockDataService : IDataService
        {
            private IEnumerable _customers;
            public void Init(IEnumerable customers)
            {
                if (customers == null)
                    throw (new Exception("IDataService.Init(customers == null)"));
            }

            public string GetCustomerName(int customerId)
            {
                if (customerId < 0)
                    throw (new Exception("IDataService.GetCustomerName: customerId cannot be negative"));
                if (customerId == 0)
                    throw (new Exception("IDataService.GetCustomerName: customerId cannot be zero"));
                return $"FirstName{customerId} LastName{customerId}";
            }
        }
    }
}

// ##template=AutoComment sha256=Qz6vshTZl2/u+NgtcV4u5W5RZMb9JPkJ2Zj0yvQBH9w
// ##template=AopCsharp.ttinclude sha256=2QR7LE4yvfWYNl+JVKQzvEBwcWvReeupVpslWTSWQ0c
// ##template=FirstDemoComment sha256=eIleHCim5r9F/33Mv9B7pcNQ/dlfEhDVXJVhA7+3OgY
// ##template=FullNameComment sha256=2/Ipn8fk2y+o/FVQHAWnrOlhqS5ka204YctZkwl/CUs
// ##template=NotifyPropertyChangedClass sha256=sxRrSjUSrynQSPjo85tmQywQ7K4fXFR7nN2mX87fCnk
// ##template=StaticAnalyzer sha256=zmJsj/FWmjqDDnpZXhoAxQB61nYujd41ILaQ4whcHyY
// ##template=LogExceptionMethod sha256=+zTre3r3LR9dm+bLPEEXg6u2OtjFg+/V6aCnJKijfcg
// ##template=NotifyPropertyChanged sha256=PMgorLSwEChpIPnEWXfEuUzUm4GO/6pMmoJdF7qcgn8
// ##template=CacheProperty sha256=oktDGTfC2hHoqpbKkeNABQaPdq6SrVLRFEQdNMoY4zE
// ##template=DependencyInjection sha256=nPq/ZxVBpgrDzyH+uLtJvD1aKbajKinX/DUBQ4BGG9g
// ##template=ResourceReplacer sha256=ZyUljjKKj0jLlM2nUIr1oJc1L7otYUI8WqWN7um6NxI


Пояснения и код шаблонов


Шаблон AutoComment

// ##aspect=AutoComment


Если в исходном коде мы встречается комментарий в специальном формате, то исполняем заданный шаблон (в данном случае это AutoComment) и вставляем результат трансформации вместо этого комментария. В этом примере имеет смысл автоматически вставлять специальный дисклаймер который предупредит программиста что код в этом файле это результат трансформации и не имеет смысла изменять этот файл напрямую.

Код шаблона AutoComment.t4

<#@ include file="AopCsharp.ttinclude" #>

//------------------------------------------------------------------------------
//  
//     This code was generated from a template.
// 
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
//
//  Generated base on file: <#= FileName #>
//  ##sha256: <#= FileSha256 #>
//  Created By: <#= User #>
//  Created Machine: <#= MachineName #>
//  Created At: <#= Now #>
//
// 
//------------------------------------------------------------------------------


Переменные FileName, FileSha256, User, MachineName и Now экспортируются в шаблон из процесса трансформации.

Результат трансформации

//------------------------------------------------------------------------------
//  
//     This code was generated from a template.
// 
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
//
//  Generated base on file: ConsoleDemo.cs
//  ##sha256: PV3lHNDftTzVYnzNCZbKvtHCbscT0uIcHGRR/NJFx20
//  Created By: EuGenie
//  Created Machine: 192.168.0.1
//  Created At: 2017-12-09T14:49:26.7173975-05:00
//
// 
//------------------------------------------------------------------------------


Следующая трансформация задаётся как атрибут класса

[AopTemplate («ClassLevelTemplateForMethods», NameFilter=«First»)]

Этот атрибут сигнализирует что шаблон необходимо применить ко всем методам класса, содержащим слово «First». Параметр NameFilter это паттерн для регулярного выражения которое применяется при определении какие методы включать в трансформацию.

Код шаблона ClassLevelTemplateForMethods.t4

<#@ include file="AopCsharp.ttinclude" #>

// class level template
<#= MethodStart() #><#= MethodBody() #><#= MethodEnd() #>


Это простейший пример который добавляет комментарий // class level template перед кодом метода

Результат трансформации

// class level template
public virtual Person FirstDemo(string firstName, string lastName, int age)
{
  Console.Out.WriteLine("FirstDemo: 1");

  // ##aspect="FirstDemoComment" extra data here

  return new Person()
      {
        FirstName = firstName,
        LastName = lastName,
        Age = age,
      };
}


Следующие трансформации задаются как атрибуты метода, для демонстрации множественных трансформации применяемых к одному и тому же методу.

[AopTemplate("LogExceptionMethod")]
[AopTemplate("StopWatchMethod")]
[AopTemplate("MethodFinallyDemo", AdvicePriority = 100)]

Шаблон LogExceptionMethod.t4

<#@ include file="AopCsharp.ttinclude" #>
<# EnsureUsing("System"); #>
<#= MethodStart() #>
try
{
<#= MethodBody() #>
} 
catch(Exception logExpn)
{
	Console.Error.WriteLine($"Exception in <#= MethodName #>\r\n{logExpn.Message}\r\n{logExpn.StackTrace}");
	throw;
}

<#= MethodEnd() #>


Шаблон StopWatchMethod.t4

<#@ include file="AopCsharp.ttinclude" #>
<# EnsureUsing("System.Diagnostics"); #>
<#= MethodStart() #>

var stopwatch = Stopwatch.StartNew(); 

try
{
<#= MethodBody() #>
} 
finally
{
	stopwatch.Stop();
	Console.Out.WriteLine($"Method <#= MethodName #>: {stopwatch.ElapsedMilliseconds}");

}

<#= MethodEnd() #>


Шаблон MethodFinallyDemo.t4

<#@ include file="AopCsharp.ttinclude" #>

<#= MethodStart() #>
try
{
<#= MethodBody() #>
} 
finally 
{
	// whatever logic you need to include for a method
}

<#= MethodEnd() #>


Результат трансформаций

public Customer[] SecondDemo(Person[] people)
{
    try
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            try
            {
                IEnumerable customers;
                Console.Out.WriteLine("SecondDemo: 1");
                {
                    // second demo test extra
                    {
                        customers = people.Select(s => new Customer()
                        {FirstName = s.FirstName, LastName = s.LastName, Age = s.Age, });
                        foreach (var customer in customers)
                        {
                            Console.Out.WriteLine($"SecondDemo: 2 {customer.FirstName} {customer.LastName}");
                        }
                    }
                }

                Console.Out.WriteLine("SecondDemo: 3");
                return customers.ToArray();
            }
            catch (Exception logExpn)
            {
                Console.Error.WriteLine($"Exception in SecondDemo\r\n{logExpn.Message}\r\n{logExpn.StackTrace}");
                throw;
            }
        }
        finally
        {
            stopwatch.Stop();
            Console.Out.WriteLine($"Method SecondDemo: {stopwatch.ElapsedMilliseconds}");
        }
    }
    finally
    {
    // whatever logic you need to include for a method
    }
}


Следующая трансформация задаётся для блока ограниченного в конструкцию using

using (new AopTemplate("SecondDemoUsing", extraTag: "test extra"))
{
    customers = people.Select(s => new Customer()
    {
        FirstName = s.FirstName,
        LastName = s.LastName,
        Age = s.Age,
    });

    foreach (var customer in customers)
    {
        Console.Out.WriteLine($"SecondDemo: 2 {customer.FirstName} {customer.LastName}");
    }
}


Шаблон SecondDemoUsing.t4

<#@ include file="AopCsharp.ttinclude" #>

// second demo <#= ExtraTag #>

<#= StatementBody() #>


ExtraTag это строка которая передаётся в параметре. Это может быть полезно для универсальных шаблонов которые могут иметь слегка отличное поведение в зависимости от входных параметров.

Результат трансформации

{
  // second demo test extra
  {
      customers = people.Select(s => new Customer()
      {FirstName = s.FirstName, LastName = s.LastName, Age = s.Age, });
      foreach (var customer in customers)
      {
          Console.Out.WriteLine($"SecondDemo: 2 {customer.FirstName} {customer.LastName}");
      }
  }
}


Следующая трансформация задаётся атрибутами класса

[AopTemplate("NotifyPropertyChangedClass", Action = AopTemplaceAction.Classes)]
[AopTemplate("NotifyPropertyChanged", Action = AopTemplaceAction.Properties)]

NotifyPropertyChanged это классический пример, который наряду с примером логирования приводится в большинстве примеров аспектно-ориентированного программирования.

Шаблон NotifyPropertyChangedClass.t4 применяется к коду класса
<#@ include file="AopCsharp.ttinclude" #>
<#
	// the class already implements INotifyPropertyChanged, nothing to do here
	if(ImplementsBaseType(ClassNode, "INotifyPropertyChanged", "System.ComponentModel.INotifyPropertyChanged"))
		return null;

	var classNode = AddBaseTypes(ClassNode, "System.ComponentModel.INotifyPropertyChanged"); 
#>

<#= ClassStart(classNode) #>
            public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

            protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
            {
                PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
            }

<#= ClassBody(classNode) #>
<#= ClassEnd(classNode) #>

Можно сравнить как подобная функциональность реализуется вплетением байт кода.
На примере реализации для Fogy
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;

public partial class ModuleWeaver
{
    public void InjectINotifyPropertyChangedInterface(TypeDefinition targetType)
    {
        targetType.Interfaces.Add(new InterfaceImplementation(PropChangedInterfaceReference));
        WeaveEvent(targetType);
    }

    void WeaveEvent(TypeDefinition type)
    {
        var propertyChangedFieldDef = new FieldDefinition("PropertyChanged", FieldAttributes.Private | FieldAttributes.NotSerialized, PropChangedHandlerReference);
        type.Fields.Add(propertyChangedFieldDef);
        var propertyChangedField = propertyChangedFieldDef.GetGeneric();

        var eventDefinition = new EventDefinition("PropertyChanged", EventAttributes.None, PropChangedHandlerReference)
            {
                AddMethod = CreateEventMethod("add_PropertyChanged", DelegateCombineMethodRef, propertyChangedField),
                RemoveMethod = CreateEventMethod("remove_PropertyChanged", DelegateRemoveMethodRef, propertyChangedField)
            };

        type.Methods.Add(eventDefinition.AddMethod);
        type.Methods.Add(eventDefinition.RemoveMethod);
        type.Events.Add(eventDefinition);
    }

    MethodDefinition CreateEventMethod(string methodName, MethodReference delegateMethodReference, FieldReference propertyChangedField)
    {
        const MethodAttributes Attributes = MethodAttributes.Public |
                                            MethodAttributes.HideBySig |
                                            MethodAttributes.Final |
                                            MethodAttributes.SpecialName |
                                            MethodAttributes.NewSlot |
                                            MethodAttributes.Virtual;

        var method = new MethodDefinition(methodName, Attributes, TypeSystem.VoidReference);

        method.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.None, PropChangedHandlerReference));
        var handlerVariable0 = new VariableDefinition(PropChangedHandlerReference);
        method.Body.Variables.Add(handlerVariable0);
        var handlerVariable1 = new VariableDefinition(PropChangedHandlerReference);
        method.Body.Variables.Add(handlerVariable1);
        var handlerVariable2 = new VariableDefinition(PropChangedHandlerReference);
        method.Body.Variables.Add(handlerVariable2);

        var loopBegin = Instruction.Create(OpCodes.Ldloc, handlerVariable0);
        method.Body.Instructions.Append(
            Instruction.Create(OpCodes.Ldarg_0),
            Instruction.Create(OpCodes.Ldfld, propertyChangedField),
            Instruction.Create(OpCodes.Stloc, handlerVariable0),
            loopBegin,
            Instruction.Create(OpCodes.Stloc, handlerVariable1),
            Instruction.Create(OpCodes.Ldloc, handlerVariable1),
            Instruction.Create(OpCodes.Ldarg_1),
            Instruction.Create(OpCodes.Call, delegateMethodReference),
            Instruction.Create(OpCodes.Castclass, PropChangedHandlerReference),
            Instruction.Create(OpCodes.Stloc, handlerVariable2),
            Instruction.Create(OpCodes.Ldarg_0),
            Instruction.Create(OpCodes.Ldflda, propertyChangedField),
            Instruction.Create(OpCodes.Ldloc, handlerVariable2),
            Instruction.Create(OpCodes.Ldloc, handl
    
            

© Habrahabr.ru