Pull to refresh

Несколько полезных аспектов для PostSharp

Reading time 11 min
Views 11K
В .net-е есть несколько серьезных AOP-фреймворков, но ни один их них не «рулит» так как PostSharp. Будучи большим фанатом (а также пользователем) сего фреймворка, хочу представить сообществу несколько «рецептов». Некоторые из них я создал сам, другие нашел в интернете и адаптировал под свои нужды. Тут я покажу несколько самых «сочных» рецептов. А если вы не знакомы с фреймворком или идеологией AOP, могу порекоммендовать вот этот вебкаст. Итак, начнем?


Потоки


Самый «стереотипичный» аспект в PostSharp – это аспект для вызова помеченного метода в пуле. Аспект очень прост, но порой бывает очень полезен:



public class WorkerThreadAttribute : OnMethodInvocationAspect<br/>
{<br/>
  public override void OnInvocation(MethodInvocationEventArgs eventArgs)<br/>
  {<br/>
    ThreadPool.QueueUserWorkItem(state => eventArgs.Proceed());<br/>
  }<br/>
}<br/>
Интересно то, что можно вызывать подобный код и получать возвращаемое значение без всяких приведений типов, как при использовании QueueUserWorkItem напрямую.

Раз у нас есть возможность «направлять» метод в определенный поток, мы с легкостью можем декларативно заставить метод исполниться в UI-потоке, как в WinForms…

public class FormsThreadAttribute : OnMethodInvocationAspect<br/>
{<br/>
  public override void OnInvocation(MethodInvocationEventArgs eventArgs)<br/>
  {<br/>
    Form f = (Form)eventArgs.Instance;<br/>
    if (f.InvokeRequired)<br/>
      f.Invoke(eventArgs.Delegate, eventArgs.GetArgumentArray());<br/>
    else<br/>
      eventArgs.Proceed();<br/>
  }<br/>
}<br/>
… так и в WPF:

public class WpfThreadAttribute : OnMethodInvocationAspect<br/>
{<br/>
  private DispatcherPriority priority;<br/>
  public WpfThreadAttribute() : this(DispatcherPriority.Normal) { }<br/>
  public WpfThreadAttribute(DispatcherPriority priority)<br/>
  {<br/>
    this.priority = priority;<br/>
  }<br/>
  public override void OnInvocation(MethodInvocationEventArgs eventArgs)<br/>
  {<br/>
    DispatcherObject dispatcherObject = (DispatcherObject)eventArgs.Instance;<br/>
    if (dispatcherObject.CheckAccess())<br/>
      eventArgs.Proceed();<br/>
    else<br/>
      dispatcherObject.Dispatcher.Invoke(priority, new Action(eventArgs.Proceed));<br/>
  }<br/>
}<br/>
В последнем случае у нас даже есть возможность выставить приоритет исполнения.

Переписывание свойств


В большинстве случаев, логика внутри set/get методов доменно-специфична, но есть один аспект который порой полезно «выносить» – это настройки уведомления об изменениях – то что называется property change notification. В WinForms у нас есть интерфейс INotifyPropertyChanged который порой так неохота реализовывать вручную. Также интерфейс этот работает и в WPF, но там есть и другая альтернатива – создание т.н. dependency property.

И то и другое решение может быть автоматически применено к обычным CLR автосвойствам. В обоих случаях решения весьма непростые, поэтому вместо кода выкладываю ссылки на реализацию INotifyPropertyChanged и Dependency Properties. К слову скажу что первым их этих сниппетов я пользовался достаточно долго, и уже вряд ли когда-нибудь буду использовать «ручную» реализацию – если только неофобы не заставят.

Ленивая инициализация


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

[Serializable]<br/>
class LazyLoad : OnFieldAccessAspect<br/>
{<br/>
  private readonly Type type;<br/>
  private readonly object[] args;<br/>
  public LazyLoad(Type type, params object[] arguments)<br/>
  {<br/>
    this.type = type;<br/>
    args = arguments;<br/>
  }<br/>
  public override OnFieldAccessAspectOptions GetOptions()<br/>
  {<br/>
    return OnFieldAccessAspectOptions.RemoveFieldStorage;<br/>
  }<br/>
  public override void OnGetValue(FieldAccessEventArgs eventArgs)<br/>
  {<br/>
    if (eventArgs.StoredFieldValue == null)<br/>
      eventArgs.StoredFieldValue = Activator.CreateInstance(type, args);<br/>
    eventArgs.ExposedFieldValue = eventArgs.StoredFieldValue;<br/>
  }<br/>
}<br/>
К сожалению, в этом коде фигурирует не очень опрятная реализация (в конструкор передается тип объекта), но с другой стороны такая реализация дает дополнительную гибкость. И конечно же, никто не мешает вместо Activator.CreateInstance вызывать, скажем, My.IoCContainer.Resolve в обход традиционной модели DI в которой выдается «все и сразу».

Попытки


Еще один классический паттерн (или антипаттерн, для тех кто ненавидит всяческий синтактический сахар) – это аспект, который позволяет вызвать метод и, в случае возникновения исключения, попробовать вызвать метод еще несколько раз с определенным временным интервалом.

[Serializable]<br/>
public class RetryAttribute : OnMethodInvocationAspect<br/>
{<br/>
  public int Count { get; set; }<br/>
  public int DelayMsec { get; set; }<br/>
  public RetryAttribute(int count) : this(count, 0) { }<br/>
  public RetryAttribute(int count, int delayMsec)<br/>
  {<br/>
    Count = count;<br/>
    DelayMsec = delayMsec;<br/>
  }<br/>
  public override void OnInvocation(MethodInvocationEventArgs eventArgs)<br/>
  {<br/>
    for (int a = 0; ; ++a)<br/>
    {<br/>
      try<br/>
      {<br/>
        if (a != 0 && DelayMsec > 0)<br/>
          Thread.Sleep(DelayMsec);<br/>
        eventArgs.Proceed();<br/>
        return;<br/>
      } <br/>
      catch<br/>
      {<br/>
        if (a == Count) throw;<br/>
      }<br/>
    }<br/>
  }<br/>
}<br/>
Использование подобного аттрибута позволяет вам модерировать «капризы» системы. Также, его видоизмененная форма может пассивно вызывать ту же функцию снова и снова, аггрегируя результаты всех вызовов. В моей практике подобный подход был полезен, например, при перечислении работающих SQL серверов в локальной сети, потому как первый вызов функции поиска находит конечно некоторые сервера, но увы, не все.

Исключения


Обработка исключений – еще один аспект разработки, который можно сделать декларативным. Вот например аспект который ловит непойманные исключения и выдает сообщение в ошокше:

[Serializable]<br/>
public class ExceptionDialogAttribute : OnExceptionAspect<br/>
{<br/>
  public override void OnException(MethodExecutionEventArgs eventArgs)<br/>
  {<br/>
    ExceptionMessageBox emb = new ExceptionMessageBox(<br/>
      eventArgs.Exception,<br/>
      ExceptionMessageBoxButtons.OK,<br/>
      ExceptionMessageBoxSymbol.Error);<br/>
    emb.Show(eventArgs.Instance as Form);<br/>
    eventArgs.FlowBehavior = FlowBehavior.Continue;<br/>
  }<br/>
}<br/>
В отличие от обычного MessageBox я воспользовался более красивым и полезным ExceptionMessageBox. Идея, в принципе, достаточно простая. И конечно этот аспект можно кастомизировать чтобы вообще игнорировать исключения определенного типа. Например, при инициализации add-in’ов Visual Studio, исключение типа ArgumentException – обычное дело, т.к. студия иногда пытается добавить элементы меню для комманд, которые уже существуют. Чтобы «проглотить» исключение, можно использовать следующий аспект:

[Serializable]<br/>
public class IgnoreExceptionAttribute : OnExceptionAspect<br/>
{<br/>
  private Type type;<br/>
  public IgnoreExceptionAttribute(Type type)<br/>
  {<br/>
    this.type = type;<br/>
  }<br/>
  public override Type GetExceptionType(System.Reflection.MethodBase method)<br/>
  {<br/>
    return type;<br/>
  }<br/>
  public override void OnException(MethodExecutionEventArgs eventArgs)<br/>
  {<br/>
    eventArgs.FlowBehavior = FlowBehavior.Continue;<br/>
  } <br/>
}<br/>
При создании сайтов на ASP.NET, у меня несколько другой подход – я выдаю «безопасную» страничку при возникновении исключения, но хочу чтобы в процессе выбрасывания исключения (которое, собственно, обработает система ASP.NET MVC) оно было перехвачено и залогированно. Для этого я использую фреймворк CodePlex.Diagnostics и вот этот простенький аспект:

[Serializable]<br/>
public sealed class InterceptExceptionAttribute : OnExceptionAspect<br/>
{<br/>
  public override void OnException(MethodExecutionEventArgs eventArgs)<br/>
  {<br/>
    IIdentity identity = WindowsIdentity.GetCurrent();<br/>
    Guid guid = ExceptionProvider.Publish(eventArgs.Exception, identity);<br/>
    throw new PublishedException(guid, eventArgs.Exception);<br/>
  }<br/>
}<br/>

Трассировка и логгирование


Раз уж заговорили о логгировании, грех не показать какой-нибудь аспект для этих целей. Не смотря на то, что большинство разработчиков имеют свои, индивидуальные подходы к этой проблеме, хочу показать просненький аспект, который записывает входы и выходы из методов, а также исключения. Для всех этих случаев, соответствующая информация выводится в окно Output студии с помощью простого вызова Debug.WriteLine:

[Serializable]<br/>
public sealed class LogAttribute : OnMethodBoundaryAspect<br/>
{<br/>
  private static int indent;<br/>
  private static int indentSize = 4;<br/>
  private static string IndentString<br/>
  {<br/>
    get<br/>
    {<br/>
      return new string(Enumerable.Range(0, indentSize*indent).Select(n => n%4 == 0 ? '|' : ' ').ToArray());<br/>
    }<br/>
  }<br/>
  private static void Indent() { ++indent; }<br/>
  private static void Unindent() { if (indent > 0) --indent; }<br/>
  private static void WriteLine(string s) { Debug.WriteLine(IndentString + s); }<br/>
  public override void OnEntry(MethodExecutionEventArgs eventArgs)<br/>
  {<br/>
    StringBuilder sb = new StringBuilder();<br/>
    sb.AppendFormat("Entering {0} with arguments", eventArgs.Method);<br/>
    object[] args = eventArgs.GetReadOnlyArgumentArray() ?? new object[] {};<br/>
    foreach (var arg in args)<br/>
    {<br/>
      sb.AppendFormat(" <{0}>", arg);<br/>
    }<br/>
    WriteLine(sb.ToString());<br/>
    Indent();<br/>
  }<br/>
  public override void OnExit(MethodExecutionEventArgs eventArgs)<br/>
  {<br/>
    Unindent();<br/>
    WriteLine("Exiting " + eventArgs.Method);<br/>
  }<br/>
  public override void OnException(MethodExecutionEventArgs eventArgs)<br/>
  {<br/>
    StringBuilder sb = new StringBuilder();<br/>
    sb.AppendFormat("Exception {0} in {1}", eventArgs.Exception, eventArgs.Method);<br/>
    sb.AppendLine();<br/>
    WriteLine(sb.ToString());<br/>
  }<br/>
}<br/>
Следует упомянуть тут, что для логгирования есть дополнение Log4PostSharp, которое записывает информацию о входе и выходе из методов с помощью подсистемы log4net.

Измерение быстродействия


Сам автор PostSharp’a в одной своей статье на CodeProject показал интересное использование аспектов для вычисления скорости исполнения методов, а также записи количества их вызовов. Базируются эти фичи на том, что в аспекте, точно как и в обычном коде, можно иметь статические члены, и вызывать их когда вздумается. Что делает возможным следующий аспект:

[Serializable]<br/>
  public class PerformanceCounterAttribute : OnMethodInvocationAspect<br/>
  {<br/>
    private static readonly Dictionary<string, PerformanceCounterAttribute><br/>
      attributes = new Dictionary<string, PerformanceCounterAttribute>();<br/>
    private long elapsedTicks;<br/>
    private long hits;<br/>
    [NonSerialized]<br/>
    private MethodBase method;<br/>
    public MethodBase Method { get { return method; } }<br/>
    public double ElapsedMilliseconds<br/>
    {<br/>
      get { return elapsedTicks / (Stopwatch.Frequency / 1000d); }<br/>
    }<br/>
    public long Hits { get { return hits; } }<br/>
    public static IDictionary<string, PerformanceCounterAttribute> Attributes<br/>
    {<br/>
      get<br/>
      {<br/>
        return attributes;<br/>
      }<br/>
    }<br/>
    public override void OnInvocation(MethodInvocationEventArgs eventArgs)<br/>
    {<br/>
      Stopwatch stopwatch = Stopwatch.StartNew();<br/>
      try<br/>
      {<br/>
        eventArgs.Proceed();<br/>
      }<br/>
      finally<br/>
      {<br/>
        stopwatch.Stop();<br/>
        Interlocked.Add(ref elapsedTicks, stopwatch.ElapsedTicks);<br/>
        Interlocked.Increment(ref hits);<br/>
      }<br/>
    }<br/>
    public override void RuntimeInitialize(MethodBase method)<br/>
    {<br/>
      base.RuntimeInitialize(method);<br/>
      this.method = method;<br/>
      attributes.Add(method.Name, this);<br/>
    }<br/>
  }<br/>
Такой вот интересный PostSharp. А какие аспекты знаете вы?
Tags:
Hubs:
+10
Comments 34
Comments Comments 34

Articles