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:
Pomimo, że Postsharp express jest w pełni darmowy, musimy zarejestrować się i uzyskać klucz:
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:
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.