Obsługa wyjątków za pomocą programowania aspektowego (Postsharp)

O programowaniu aspektowym kiedyś już pisałem więc jeśli od strony teoretycznej nie jest to jasne to zachęcam do poszperania na blogu. Dzisiaj zaprezentuje framework Postsharp w wersji express (darmowa edycja, również do zastosowań komercyjnych). Jak wiemy, obsługa wątków czy wykonanie logów mogą być problemami cross-cutting. Postsharp jest typowym framework’iem implementującym AoP Zaczynamy od instalacji z NuGet:

image

 

Pomimo, że Postsharp express jest w pełni darmowy, musimy zarejestrować się i uzyskać klucz:

image

 

Następnie możemy zdefiniować nasz pierwszy aspekt, który uruchomi się w momencie wyrzucenia wątku. Generalnie w Postsharp istnieje kilka klas bazowych dla aspektów. W naszym przypadku skorzystamy z OnMethodBoundaryAspect, która zawiera kilka ciekawych metod. Implementacja aspektu polega po prostu na dziedziczeniu z OnMethodBoundaryAspect i przeładowaniu kilku metod. Zaglądając do dokumentacji dowiemy się trochę więcej o OnMethodBoundaryAspect:

[SerializableAttribute]
public abstract class OnMethodBoundaryAspect : MethodLevelAspect, 
    IOnMethodBoundaryAspect, IMethodLevelAspect, IAspect

Aby zobaczyć jakie metody możemy przeładować i kiedy są one wywołane przyjrzyjmy się następującemu pseudo-kodowi:

int MyMethod(object arg0, int arg1)
{
  OnEntry();
  try
  {
    // Original method body. 
    OnSuccess();
    return returnValue;
  }
  catch ( Exception e )
  {
    OnException();
  }
  finally
  {
    OnExit();
  }
}

Innymi słowy, mając metodę MyMethod, OnEntry zostanie wywołane przy wejściu do niej, OnSucces po wykonaniu logiki w niej zawartej, OnException w przypadku wyjątku, a OnExit zawsze przy wyjściu (niezależnie czy był wyjątek czy nie).

W naszym przypadku będziemy potrzebować tylko OnException. Zaimplementujmy więc pierwszy aspekt (zmodyfikowany przykład z oficjalnej dokumentacji Postsharp):

[Serializable]
public class ExceptionAspect : OnMethodBoundaryAspect
{
   private string methodName;
   
   public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
   {
       Debug.Assert(method.DeclaringType != null, "method.DeclaringType != null");
       methodName = method.DeclaringType.FullName + "." + method.Name;
   }
   public override void OnException(MethodExecutionArgs args)
   {
       Trace.Unindent();

       StringBuilder stringBuilder = new StringBuilder(1024);

       // Write the exit message. 
       stringBuilder.Append("Error:");
       stringBuilder.Append(this.methodName);
       stringBuilder.Append('(');

       // Write the current instance object, unless the method 
       // is static. 
       object instance = args.Instance;
       if (instance != null)
       {
           stringBuilder.Append("this=");
           stringBuilder.Append(instance);
           if (args.Arguments.Count > 0)
               stringBuilder.Append("; ");
       }

       // Write the list of all arguments. 
       for (int i = 0; i < args.Arguments.Count; i++)
       {
           if (i > 0)
               stringBuilder.Append(", ");
           stringBuilder.Append(args.Arguments.GetArgument(i) ?? "null");
       }

       // Write the exception message. 
       stringBuilder.AppendFormat("): Exception ");
       stringBuilder.Append(args.Exception.GetType().Name);
       stringBuilder.Append(": ");
       stringBuilder.Append(args.Exception.Message);

       // Finally emit the error. 
       Console.WriteLine(stringBuilder.ToString());
   }
} 

Metoda CompileTimeInitialzie jest wykonywana zawsze gdy aspekt jest doczepiony do konkretnej metody. Następnie w OnException pobieramy przydatne informacje takie jak nazwa metody, lista przekazanych parametrów czy stacktrace. Myślę, że możliwość wyprintowania parametrów i ich wartości jest niesłychanie przydatna. Oprócz tego, że będziemy wiedzieć gdzie wyjątek został wyrzucony, aspekt dostarczy nam informacji o konkretnym scenariuszu.

To nie koniec. Musimy zdefiniować zasady aplikowania aspektu. Najpierw spróbujmy doczepić aspekt do konkretnej, pojedynczej metody np.:

internal class Program
{
   private static void Main()
   {
       MethodCausingError("Witaj swiecie.");
   }   
   [ExceptionAspect]
   private static void MethodCausingError(string text)
   {
       throw new ArgumentException("Jakis blad.");
   }
}

Na ekranie, zobaczymy przechwycony wyjątek, wraz z parametrami:

image

 

W praktyce jednak, dużo lepiej skorzystać z bardziej globalnego mechanizmu tzn.:

[assembly: ExceptionAspect(AttributeTargetTypes = "*")] 

Powyższa linijka kodu zaaplikuje aspekt na każdej metodzie. Można również określić filtr, np. poprzez zdefiniowanie namespace:

[assembly: OurLoggingAspect(AttributeTargetTypes= 
"OurCompany.OurApplication.Controllers.*")] 

jeśli jest to nie wystarczające to można określić również widoczność typu (private, public itp.) czy typ metody:

[assembly: OurLoggingAspect 
(AttributeTargetTypes="OurCompany.OurApplication.Controllers.*",  
AttributeTargetTypeAttributes = MulticastAttributes.Public)]
// II
[assembly: OurLoggingAspect 
(AttributeTargetTypes="OurCompany.OurApplication.Controllers.*",  
AttributeTargetMemberAttributes = MulticastAttributes.Virtual)] 

W zasadzie warto na koniec zaznaczyć, że dla tego przykładu lepiej byłoby skorzystać z OnExceptionAspect zamiast OnMethodBoundaryAspect ponieważ wystarczy np. wyłącznie metoda OnException. To tylko takie wprowadzenie, w następnych wpisach pokażę bardziej szczegółowo sterowanie przepływem metod.

Leave a Reply

Your email address will not be published.