Специализированные атрибуты (часть 2)

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

Первая статья данной серии вводит читателя в ключевые принципы PostSharp, основываясь на простом примере: трассируемом специализированном атрибуте, полученном из OnMethodBoundaryAspect. Данная статья представляет другие два типа специализированных атрибутов с двумя примерами: счетчик производительности, основанный на OnMethodInvocationAspect, и структура проверки полей, основанная на OnFieldAccessAspect.

Двойное использование специализированных атрибутов

Перед тем, как мы перейдем к реализации новых специализированных атрибутов, давайте рассмотрим внутреннюю работу PostSharp Laos. PostSharp является расширителем функциональных возможностей MSIL: он внедряет себя в процесс сборки, и модифицирует результат компилятора (C#, VB.NET, J#, …). Он ищет специализированные атрибуты PostSharp Laos, модифицирует методы, типы и поля, к которым они применены.

Чтобы получить больше выгоды от PostSharp Laos, необходимо полностью понимать жизненный цикл его специализированных атрибутов. Они применяются дважды: впервые при компиляции в PostSharp; второй раз - во время выполнения.

Жизненный цикл специализированных атрибутов PostSharp Laos:

  • При компиляции :
  1. Для каждого применения специализированных атрибутов создается новый экземпляр. Поэтому, экземпляр специализированного атрибута всегда назначается одному и только одному методу, полю или типу. Затем экземпляры инициализируются (метод CompileTimeInitialize) и проверяются на валидность (метод CompileTimeValidate).
  2. Специализированные атрибуты сериализуются в большой двоичный объект (blob).
  3. Они хранятся в управляемом ресурсе результирующей сборки.
  • Во время выполнения :
  1. Специализированные атрибуты восстанавливаются из управляемого ресурса и каждый экземпляр инициализуется второй раз (метод RuntimeInitialize),
  2. Методы исполнения (OnEntry, OnExit, …) вызываются при вызове или осуществлении доступа к методу или полю, к которому они применены.

Пока теории достаточно, потому перейдем к коду.

Специализированный атрибут подсчета производительности

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

А что, если мы хотим, чтобы регулирующий код находился за пределами текущей сборки? Это не будет проблематичным в PostSharp - специализированный атрибут будет применен к внешним объявлениям. Тем не менее, поскольку мы не можем изменить метод, нам необходимо перехватить его вызов. Итак, вместо аспекта OnMethodBoundary мы будем использовать OnMethodInvocation:

[Serializable]
public class PerformanceCounterAttribute : OnMethodInvocationAspect
{

    public override void OnInvocation( MethodInvocationEventArgs eventArgs )
    {
       // наша реализация расположена здесь.
    }
}

В параметре eventArgs, мы получаем информацию о методе, который был по-настоящему вызван: eventArgs.DelegateeventArgs.GetArguments() дает аргументы, передаваемые методу. PostSharp Laos ожидает вставку возвращаемого значения в eventArgs.ReturnValue. Поэтому мы можем вызвать перехватываемый метод следующим образом: является делегатом перехвачиваемого метода, а

eventArgs.ReturnValue = eventArgs.Delegate.DynamicInvoke(
                                             eventArgs.GetArguments() );

1. Измерение производительности

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

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

[Serializable]
public class PerformanceCounterAttribute : OnMethodInvocationAspect
{
    private long elapsedTicks;
    private long hits;

    public override void OnInvocation( MethodInvocationEventArgs eventArgs )
    {
        Stopwatch stopwatch = Stopwatch.StartNew();
         
        try
        {
           
            eventArgs.ReturnValue = eventArgs.Delegate.DynamicInvoke(
                                                  eventArgs.GetArguments() );
        }
        finally
        {
            stopwatch.Stop();
            Interlocked.Add( ref this.elapsedTicks, stopwatch.ElapsedTicks );
            Interlocked.Increment( ref this.hits );
        }
    }
}

2. Нахождение экземпляров счетчиков производительности

Как же нам считать значения? Нам просто необходимо получить доступ к полям в свойствах, доступных только для чтения (public read-only), и собрать репозиторий экземпляров счетчиков производительности. Нам необходимо создать данный репозиторий во время выполнения - поэтому, мы не можем сделать это во время регистрации в конструкторе экземпляров (который вызывается во время компиляции), но должны сделать это в методе RuntimeInitialize. Кое-что напоследок: нам необходимо раскрыть измеряемый метод, а иначе, как мы узнаем, к какому методу относится счетчик? Итак, нам необходимо хранить целевой метод в поле и раскрыть его в свойстве, доступном только для чтения. Метод RuntimeInitialize является верным местом для инициализации данного поля.

Следующий код необходимо добавить к классу:

[NonSerialized] private MethodBase method;

private static readonly List<performancecounterattribute /> instances =
              new List<performancecounterattribute />();

public override void RuntimeInitialize( MethodBase method )
{
   base.RuntimeInitialize( method );
   this.method = method;
   instances.Add( this );
}
public MethodBase Method { get { return this.method; } }

public double ElapsedMilliseconds
{
  get { return this.elapsedTicks/( Stopwatch.Frequency/1000d ); }
}

public long Hits { get { return this.hits; } }

public static ICollection<performancecounterattribute /> Instances
{
  get
  {
    return new ReadOnlyCollection<performancecounterattribute />( instances );
  }
}

3. Использование готового решения

И это все! Мы теперь можем применить наш специализированный атрибут к тем методам, которые мы хотим измерить. Допустим мы хотим измерить потраченное время в расширении имен System.IO. Мы добавим счетчик производительности к данным методам используя следующий код:

[assembly: PerformanceCounter(AttributeTargetAssemblies = "mscorlib", 
           AttributeTargetTypes = "System.IO.*")]

И если мы проверим маленькую программу, которая перечисляет содержимое каталога, у нас будет следующий результат:

Вам стоит быть в курсе того, что данный аспект перехватывает только вызовы, которые были сделаны из текущей сборки. Потому, если вы вызовете внешний метод, который косвенно вызывает измеряемый метод, данный вызов не будет наблюдаться. Данное ограничение присуще технологии, используемой переписанным PostSharp: MSIL.

Проверка валидности полей при помощи специализированных атрибутов

Итак, мы увидели то, как можно модифицировать тело метода и как можно перехватывать вызовы методов. PostSharp позволяет перехватывать операции "get" и "set" по отношению к полям. Одним из применений данного метода является валидация полей: мы можем сделать поле ненулевым или выработать регулярное выражение просто оформляя поле специализированным атрибутом.

Аспекты, которые должны перехватывать доступ к полям, должны наследовать класс OnFieldAccessAspect. Они могут переписать методы OnGetValue() и OnSetValue(). Для валидации полей мы заинтересованы только во втором методе. Все, что нам необходимо, это выполнить валидацию, которая будет специфична для контрольного параметра.

1. Разработка абстрактной структуры

Разработка класса структуры проверки является простой процедурой - у нас практически абстрактный класс FieldValidationAttribute , который раскрывает абстрактный метод Validate() , вызванный из перегруженного метода OnSetValue(). По контракту, реализация Validate() должна создавать исключительную ситуацию в случае, если метод неверен. Исключение обычно включает в себя название поля. Итак, все нормально, если класс FieldValidationAttributeCompileTimeInitialize() и хранится в сериализуемом поле. раскрывает имя поля, к которому применен специализированный атрибут. Поскольку данная информация известна во время компиляции, он инициализируется в методе

Вам стоит помнить, что также, как OnMethodInvocationAspect, OnFieldAccessAspect перехватывает доступ к полю - поэтому, все ограничено текущей сборкой. Если следовать рекомендациям компании Microsoft и иметь только закрытые поля (private), то это не будет проблемой. Но если у вас будут общедоступные поля (public), вам стоит попросить PostSharp Laos инкапсулировать поля в свойства. Просто перегрузите метод GetOptions() и верните GenerateProperty.

Вот полный код класса FieldValidationAttribute:

[Serializable]
[AttributeUsage( AttributeTargets.Field, AllowMultiple = false )]
public abstract class FieldValidationAttribute : OnFieldAccessAspect
{
    private string fieldName;

    public override void CompileTimeInitialize( FieldInfo field )
    {
        base.CompileTimeInitialize( field );

        this.fieldName = field.DeclaringType.Name + "." + field.Name;
    }

    public string FieldName { get { return this.fieldName; } }

    protected abstract void Validate( object value );

    public override sealed void OnSetValue( FieldAccessEventArgs eventArgs )
    {
        this.Validate( eventArgs.ExposedFieldValue );

        base.OnSetValue( eventArgs );
    }

    public override OnFieldAccessAspectOptions GetOptions()  
    {
        return OnFieldAccessAspectOptions.GenerateProperty;
    }

}

2. Проверка ненулевых полей

Аспект ненулевого поля является наиболее тривиальным:

[Serializable]
public sealed class FieldNotNullAttribute : FieldValidationAttribute
{
    protected override void Validate( object value )
    {
        if ( value == null )
            throw new ArgumentNullException( "field " + this.FieldName );
    }
}

Определение ненулевого поля не является сложным действием:

class MyClass
{
  [FieldNotNull]
  public string Name = "DefaultName";
}

3. Проверка посредством регулярных выражений

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

Вот основная, но рабочая реализация валидатора поля на основе сопоставления шаблона:

[Serializable]
public sealed class FieldRegexAttribute : FieldValidationAttribute
{
    private readonly string pattern;
    private readonly bool nullable;
    private RegexOptions regexOptions = RegexOptions.Compiled;

    [NonSerialized]
    private Regex regex;

    public FieldRegexAttribute(string pattern, bool nullable)
    {
        this.pattern = pattern;
        this.nullable = nullable;
    }

    public FieldRegexAttribute(string pattern) : this(pattern, false)
    {
    }

    public RegexOptions RegexOptions
    {
        get { return regexOptions; }
        set { regexOptions = value; }
    }

    public override void RuntimeInitialize(FieldInfo field)
    {
        base.RuntimeInitialize(field);
        this.regex = new Regex( this.pattern, this.regexOptions);
    }

    protected override void Validate( object value )
    {
        if ( value == null )
        {
            if ( !nullable )
            {
                throw new ArgumentNullException("field " + this.FieldName);
            }
        }
        else
        {
            string str = (string) value;
            if ( !this.regex.IsMatch( str ))
            {
                throw new ArgumentException(
                    "The value does not match the expected pattern.");
            }
        }
    }
}

4. Использование готового решения

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

Их использование довольно просто:

class MyClass
{
    [FieldNotNull]
    public string Name = "DefaultName";
    
    [FieldRegex(@"^([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|
                 (([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$")]
    public string EmailAddress;
}

Посмотрите на результирующую сборку при помощи рефлектора Lutz Roeder Reflector:

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

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

Вывод

Хотя первая статья данной серии повествовала о большинстве ключевых принципов PostSharp Laos при помощи простого примера на основе OnMethodBoundaryAspect, данная статья ввела два новых аспекта: OnMethodInvocation и OnFieldAccess.

Мы уже видели разницу между OnMethodBoundaryAspect и OnMethodInvocationAspect: в то время, как первый на самом деле пытается добавить блок try-catch к целевому методу, второй перехватывает вызовы метода и не модифицирует целевой метод. Это позволяет применить OnMethodInvocationAspect даже к тем методам, которые были определены за пределами текущей сборки. Первый пример использовал данную функциональность для измерения времени, потраченного в пространстве имен System.IO.

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

Мы надеемся, что в основном убедили вас в том, что хотя Аспектно-ориентированное программирование (АОП) предлагает элегантное решение большинства похожих проблем, PostSharp Laos предоставляет более простое и мощное решение. Ведь PostSharp позволяет сэкономить немало усилий и времени.

Автор:  Gael Fraiteur

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