#3 CastleCzęść NR.3

W poprzednim wpisie pokazałem prosty przykład AOP z PostSharpem.

Teraz pokażę wam podobny prosty przykład AOP, ale z Interceptorami z Castle.Windosor.

Różnica tej techniki z PostSharpem jest duża.

Kontener wstrzykiwania zależności Castle będzie dodawał aspekty w trakcie wykonywania programu. PostSharp robił to poprzez dodatkowy proces kompilacyjny.

Warto też zaznaczyć, że ten przykład będzie się różnił od poprzedniego przykładu. Dlaczego?

Muszę wyjaśnić czym jest intercepcja metody

Intercepcja metody

Intercepcja metody jest kawałkiem kodu, który uruchamia się zamiast metody, na której wykonujemy tę operację.

Wcześniej w PostSharp mieliśmy OnEnter i OnExit. One akurat nie obrazują Intercepcji ponieważ nie mogą zablokować działania oryginalnej metody. One tylko dodają dodatkowy kod wykonujący.

W PostSharp  intercepcja metody jest możliwa. Wymaga to tylko innej klasy bazowej, a dokładniej klasy “MethodInterceptionAspect”.

Ten przykład pokażę później.

Skoncentrujmy się na Castle.Windsor.

Intercepcja więc może zablokować działanie oryginalnej metody. Pytanie, czy rzeczywiście posiadanie człowieka pośrodku decydującego, czy może wykonać daną metodę, czy nie, to dobry pomysł?

Jednak tak, jak z człowiekiem pośrodku, w prawdziwym życiu intercepcja ma bardzo ważny cel.

Używając intercepcji możemy tak, jak wcześniej dodać kod przed i po wywołaniu metody. Możemy jednak też decydować, czy metoda powinna zostać uruchomiona. Oto przykład z pierwszego wpisu na temat AOP.

GameId

Jeśli obiekt Game istnieje w obiekcie Cashe, to nie muszę pytać bazy danych o ten obiekt.

Oczywiście nie jest to jedyna możliwość.

Przykładowo chcemy wysłać komunikat do serwera. W przypadku nieudanej próby nasz interceptor mógłby wykonywać to zadanie z 10 razy, aż do momentu sukcesu.

Nasz Interceptor mógłby zmienić wartość parametrów, jeśli są nieprawidłowe.

Możliwości jest wiele.

Pamiętaj jednak, że Interceptor nie zastępuje całej metody. W większości wypadków aspekt pozwala na dalszą egzekucję kodu.

Teraz, gdy wyjaśniliśmy sobie co to jest Intercepcja stwórzmy prosty przykład z Castle.Windsor.

Castle.Core

Podobnie jak wcześniej stwórzmy aplikację konsolową.

A teraz dodajmy do niej potrzebne referencje do Castle.Windsor. Robimy to przy pomocy systemu paczek NuGet.

W polu wyszukania wpisz Castle.Core.

Castle.Core

Teraz jesteśmy gotowi. Podobnie jak w ostatnim przykładzie mam dwie klasy biznesowe.

public class MotherFBussinesClass
{
    public void Method1()
    {
        Console.WriteLine("I am MotherFBussinesClass");
    }
}

public class AnotherBussinesClass
{
    public void Method1()
    {
        Console.WriteLine("I am AnotherBussinesClass");
    }
}

Chcę do nich dodać kod przed i po wywołaniu Method1. Jak to teraz zrobimy?

Utwórz klasę “MyInterceptorAspect” , musi ona implementować interfejs IInterceptor.

using Castle.DynamicProxy;
using System;

namespace CastleInterceptionHello
{
    public class MyInterceptorAspect : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            throw new NotImplementedException();
        }
    }
}

Teraz jak powiedzieć Castle.DynamicProxy, jaki kod chcemy interceptować?

W PostSharpie wybierasz pojedynczą metodę. W Castle DynamicProxy do intercepcji wybierasz całe obiekty.

DynamicProxy może wybierać indywidualne metody, ale i tak stworzy on otoczkę dla całej klasy.

Dodanie interceptora  z DynamicProxy to proces dwukrokowy:

  • Musimy stworzyć ProxyGenerator
  • Potem go użyć aby dodać interceptor

Utworzenie ProxyGeneator jest proste. Wystarczy stworzyć jego instancję.

Używając generatora stworzymy opakowany obiekt z naszym interceptorem.

static void Main(string[] args)
{
    var proxyGenerator = new ProxyGenerator();

    var obj = proxyGenerator
        .CreateClassProxy<MotherFBussinesClass>(
            new MyInterceptorAspect()
        );

    var obj2 = proxyGenerator
        .CreateClassProxy<AnotherBussinesClass>(
            new MyInterceptorAspect()
        );

    obj.Method1();
    obj2.Method1();
}

Zanim jednak uruchomisz aplikację warto wspomnieć o pewnym krytycznym kroku, który musi zostać wykonany, jeśli chcemy by interceptor zadziałał.

Każda metoda biznesowa, która będzie zinterceptorowana musi być wirtualna. W przeciwnym wypadku interceptor się nie wykona.

public class MotherFBussinesClass
{
    public virtual void Method1()
    {
        Console.WriteLine("I am MotherFBussinesClass");
    }
}

public class AnotherBussinesClass
{
    public virtual void Method1()
    {
        Console.WriteLine("I am AnotherBussinesClass");
    }
}
Castle DynamicProxy i metody wirtualne

Alternate Text Generator proxy Castle DynamicProxy nie zwraca obiektu klasy MotherFBussinesClass, ale typ, który został wygenerowany dynamicznie i jego klasę bazową MotherFBussinesClass.

Oznacza to, że każda metoda, którą chcesz zintercepterować musi być wirtualna, inaczej Castle nie doda funkcjonalności.

Jeśli korzystasz lub korzystałeś z NHibernate to wiesz dlaczego taki wymóg istnieje. Niespodzianka NHibernate korzysta z Castle DynamicProxy.

Jeśli ten wymóg wydaje ci się straszny. To zapamiętaj, że normalnie w takich wypadach programuje się przy użyciu interfejsów. Pokażę to jeszcze w tym wpisie.

Gdy tak zrobimy, wymóg co do metod wirtualnych nie pojawi się.

Teraz interceptujemy wywołanie metody.

Co możemy zrobić wewnątrz  klasy aspektowej. Wewnątrz obiektu invocation mamy wiele ciekawych metod i właściwości.

Intercep Invocation

Analogicznie do poprzedniego przykładu wyświetlane są, na początku i na końcu metody napisy w Konsoli.

W obiekcie invocation jestem w stanie znaleźć nazwę metody biznesowej i jej klasę oraz moduł, w którym się ona znajduje.

public class MyInterceptorAspect : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        WriteMessage(invocation, "Before method ", ConsoleColor.DarkYellow);
        invocation.Proceed();
        WriteMessage(invocation, "End method ", ConsoleColor.Yellow);
    }

    public void WriteMessage(IInvocation invocation,string message,ConsoleColor color)
    {
        Console.ForegroundColor = color;
        Console.WriteLine(message +
            invocation.Method.DeclaringType.Name + "." + invocation.Method.Name
            + " in " + invocation.Method.Module.Name);
        Console.ForegroundColor = ConsoleColor.Gray;
    }
}

Działanie programu wygląda jak poprzednio.

To samo

Teraz jednak, gdy mamy interceptor możemy trochę się pobawić metodą biznesową.

Przykładowo mogę ją wywołać częściej niż raz, dzięki mojemu aspektowi.

public class MyInterceptorAspect : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        WriteMessage(invocation, "Before method ", ConsoleColor.DarkYellow);
    
        Random random = new Random(Guid.NewGuid().GetHashCode());
    
        for (int i = 0; i < random.Next(10); i++)
        {
            invocation.Proceed();
        }
    
        WriteMessage(invocation, "End method ", ConsoleColor.Yellow);
    }

Jak widać rzeczywiście to tak działa.

Random

Mogę też postawić warunek wywołania metody biznesowej.

Przykładowo jeśli metoda nie ma żadnych argumentów, to wtedy może być ona uruchomiona.

public void Intercept(IInvocation invocation)
{
    WriteMessage(invocation, "Before method ", ConsoleColor.DarkYellow);

    if (invocation.Arguments.Length == 0)
        invocation.Proceed();

    WriteMessage(invocation, "End method ", ConsoleColor.Yellow);
}

Mój aspekt może też obsłużyć błędy aplikacji.

Teraz nie będę musiał otaczać wszystkich metod biznesowych klazurą try-catch-finally.

public void Intercept(IInvocation invocation)
{
    WriteMessage(invocation, "Before method ", ConsoleColor.DarkYellow);
    try
    {
        invocation.Proceed();
    }
    catch (Exception ex)
    {
        WriteMessage(invocation, "Error " + ex.Message + " ", ConsoleColor.Red);
    }
    finally
    {
        WriteMessage(invocation, "End method ", ConsoleColor.Green);
    }
}

Wyrzućmy wyjątek w jednej z metod biznesowych i zobaczmy co się stanie.

public virtual void Method1()
{
    throw new ArgumentException("LOL EROROR");
    Console.WriteLine("I am MotherFBussinesClass");
}

Błąd został wyświetlony na czerwono.

Error

To jest bardzo bazowy przykład użycia Castle.Dynamic proxy.

W prawdziwym życiu jednak nikt by tak tej funkcji nie użył.

Przejdźmy więc bardziej do prawdziwego świata wstrzykiwania zależności i zainstalujmy bibliotekę Castle.Windsor przy użyciu NuGet.

Castle Windsor

Zróbmy małą modyfikację w kodzie.

Nasze klasy biznesowe mają teraz interfejsy. Teraz mógłbym się pozbyć słów kluczowych virtual na metodach.

public interface IMotherFBussinesClass
{
    void Method1();
}
public class MotherFBussinesClass : IMotherFBussinesClass, IThisIncept
{
    public virtual void Method1()
    {
        Console.WriteLine("I am MotherFBussinesClass");
    }
}

public interface IAnotherBussinesClass
{
    void Method1();
}

public class AnotherBussinesClass : IAnotherBussinesClass, IThisIncept
{
    public virtual void Method1()
    {
        Console.WriteLine("I am AnotherBussinesClass");
    }
}

Teraz tworzę kontener wstrzykiwania zależności. Niestety o kontenerach wstrzykiwania zależności można opowiadać dosyć długo, a ten wpis na tym się nie koncentruje.

W kontenerze wstrzykiwania zależności  Castle.Windsor deklaruję, że moje interfejsy biznesowe są odpowiednio zaimplementowane przez klasy.

W kontenerze także mówię, że moje komponenty powinny mieć także interceptory.

Aby wszytko zadziałało poprawnie. Swój interceptor też muszę zarejestrować do kontenera.

IWindsorContainer container = new WindsorContainer();

container.Register(
    Component.For<MyInterceptorAspect>(),
    Component.For<IAnotherBussinesClass>().ImplementedBy<AnotherBussinesClass>()
    .Interceptors<MyInterceptorAspect>(),
    Component.For<IMotherFBussinesClass>().ImplementedBy<MotherFBussinesClass>().
    Interceptors<MyInterceptorAspect>()
);

var obj3 = container.Resolve<IMotherFBussinesClass>();
var obj4 = container.Resolve<IAnotherBussinesClass> ();

obj3.Method1();
obj4.Method1();

Teraz używając kontenera robię resolve na interfjesach. To co otrzymam będzie dynamicznym typem, którego klasa bazowa jest klasą biznesową.  W tym obiekcie będzie żył kod mojego aspektu.

Oczywiście tak wygląda bardzo boleśnie. Zwłaszcza jeśli mamy klas biznesowych ze 100.

Castle.Windsor jest poważnym narzędziem. Proces rejestracji komponentów można wykonać w stylu określenia zbiorowej zasady do rejestracji.

container.Register(
    Component.For<MyInterceptorAspect>());

container.Register(Classes.FromThisAssembly()
        .InSameNamespaceAs<IAnotherBussinesClass>()
        .WithService.DefaultInterfaces()
        .Configure(delegate (ComponentRegistration c)
    {

        var x = c.Interceptors(InterceptorReference.
            ForType<MyInterceptorAspect>()).Anywhere;
    }));


var obj3 = container.Resolve<IMotherFBussinesClass>();
var obj4 = container.Resolve<IAnotherBussinesClass>();

obj3.Method1();
obj4.Method1();

Co tutaj się dzieje? Kontener rejestruje wszystkie klasy znajdujące się w przestrzeni nazw jak interfejs “IAnotherBussinesClass” i podpina ich domyślne interfejsy do nich. 

W metodzie configure mogę dodać delegatę. W delegacie mam obiekt rejestrowanego komponentu.

Mając ten obiekt mogę dodać do niego interceptor.

W ten sposób do wszystkich klas z interfejsami w przestrzeni nazw interfejsu “IAnotherBussinesClass” dodałem interceptor.

Na koniec nie zapomnij o właściwości “Anywere”. W przeciwnym wypadku interceptor nie zainstaluje się nigdzie.

Anyware Castle

Więcej o tym możesz przeczytać tutaj.

http://docs.castleproject.org/Windsor.Registering-Interceptors-ProxyOptions.ashx

Właściwość jak “Anywhere”,”First”, “Last” określają kolejność wstrzykiwania interceptora,.

Jest to pomocne, gdy chcesz wstrzyknąć wiele interceptorów i kolejność ich działania ma znaczenie.

W następnym wpisie zrobimy to samo, tylko przy użyciu PostSharp.