Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

Przygody z Policy Injection Application Block

Policy Injection Application Block w skrócie to biblioteka pozwalająca na wstrzyknięcie kodu opakowującego wywołania metod. Dzięki temu za pomocą jednego atrybutu lub odpowiedniego wpisu w pliku konfiguracyjnym, możemy "nakazać", aby np. czas wykonania metody był mierzony i logowany. Oczywiście, różnych takich zastosowań możemy wyobrazić sobie bardzo wiele. Tyleż samo widać korzyści z zastosowania tego podejścia. Programowanie aspektowe (inaczej AOP - aspect oriented programming), o którym właśnie mowa, pozwala ograniczyć ilość powtarzalnego kodu, tak aby programista mógł skoncentrować na implementowanej funkcjonalności, a architekt mógł zaimplementować raz a dobrze kluczowe kwestie (logowanie, obsługa wyjątków, transakcje, itp.).

Pierwsze zauroczenie...

Moją przygodę na poważnie z PIAB zacząłem jakiś czas temu, kiedy postanowiłem wykorzystać ten blok w jednym z projektów. Spośród istniejących rozwiązań wybrałem właśnie blok z Enterprise Library, kierując się przy tym po części instynktem, a po części zaufaniem do technologii sygnowanych przez samą grupę Patterns & Practices z Microsoft. Początki były bardzo miłe - zalety programowania aspektowego są oczywiste. Ponadto, wraz z PIAB dostajemy zestaw gotowych aspektów (a właściwie "call handlers"), integrujących blok z resztą biblioteki. Tak więc wstrzyknięcie kodu do logowania, obsługi wyjątków, walidacji, autoryzacji, itd. uzyskujemy dodając w najprostszym przypadku jeden atrybut.

Wykorzystanie samego PIAB jest proste i nie wymaga żadnych skomplikowanych czynności. Jeżeli planujemy dodawać aspekty tylko poprzez atrybuty, to nawet nie potrzebujemy dodatkowych wpisów w pliku konfiguracyjnym. Wystarczy tylko w specjalny sposób "opakować" obiekt, w który chcemy wstrzyknąć dodatkową funkcjonalność. Oto prosty przykład pokazujący, w jaki sposób uzyskać walidację parametrów metody:

public interface ILogic
{
    [ValidationCallHandler]
    void SayMyName([NotNullValidator]string name);
}

public class Logic : ILogic
{
    public void SayMyName(string name)
    {
        Console.WriteLine(name);
    }
}
Listing 1. Przykład zastosowania PIAB.

Atrybut ValidationCallHandlerAttribute powoduje, iż przed uruchomieniem właściwego kodu metody uruchamiana jest walidacja parametrów. Atrybut NotNullValidator pochodzi już z Validation Application Block, a jego działanie jest chyba oczywiste.

Aby PIAB zadziałało potrzeba jeszcze opakować obiekt klasy Logic:

ILogic logic = PolicyInjection.Wrap<ILogic>(new Logic());

Oczywiście teraz po wywołaniu kodu:

logic.SayMyName(null);

dostaniemy wyjątek ArgumentValidationException, choć nigdzie jawnie nie dokonywaliśmy sprawdzania ani wyrzucania wyjątku. Wygodne, prawda?

...a po upływie miesiąca miodowego...

Zawsze po miłych początkach, nadchodzą pierwsze rozczarowania. Tak samo było, niestety, z moim "romansem" z PIAB. Zanim jednak przejdę do rzeczy, warto zajrzeć pod maskę i zobaczyć jak PIAB działa.

Otóż, podstawowym wymaganiem, by móc wstrzykiwać polityki do klasy jest aby:

  • implementowała ona interfejs przez który będziemy się do niej odwoływać lub
  • dziedziczyła po MarshalByRefObject.

Wymagania te wynikają stad, iż PIAB wewnętrznie wykorzystuje mechanizmy z .NET Remoting i magię CLR, która pozwala utworzyć transparentne proxy dla interfejsów lub właśnie typów dziedziczących po MarshalByRefObject. Owe transparentne proxy przekazują wywołanie do rzeczywistego proxy, którego zadaniem jest utworzenie ciągu odpowiednich call handlers (aspektów). Ostatnim na liście będzie zawsze kod dokonujący rzeczywistego wywołania docelowej metody. Poniższy diagram UML prezentuje jak to w uproszczeniu wygląda:

piab 
Diagram 1. Schematyczna struktura klas przy przechwytywaniu wywołań metod przez PIAB.

Jak widać ciąg wywołań będzie następujący:

  1. Użytkownik wywołuje metodę na transparent proxy.
  2. Dane o wywołaniu zostają zebrane (wartości argumentów, metoda, itp.) i trafiają do metody Invoke() klasy InterceptingRealProxy.
  3. Informacje o wywołaniu oraz delegat wywołujący następny call handler trafiają do pierwszego handlera. Handler może opakować swoją funkcjonalnością dalsze wywołanie.
  4. Wywołanie trafia stopniowo do wszystkich handlerów, aż na samym końcu pojawia się kod, który przez refleksję wywołuje docelową metodę. Wygląda on mniej więcej tak:
       1: try
       2: {
       3:     object returnValue = call.MethodBase.Invoke(/*..*/);
       4:     return /*...*/; //obiekt z informacją o powodzeniu i rezultatem
       5: }
       6: catch (TargetInvocationException ex)
       7: {
       8:     return /*..*/; //obiekt z informacją o wyjątku ex.InnerException
       9: }
    Listing 2. Wywołanie metody docelowej przez infrastrukturę PIAB.

Warto zwrócić uwagę na linijki 6-9, gdyż sygnalizują one mój pierwszy, na razie drobny problem.

Wyjątek wyrzucany dwa razy

Oto pierwsza niespodzianka - wywołując poniższy kod z Visual Studio:

public class Logic : MarshalByRefObject
{
    [ValidationCallHandler]
    public void SayMyName([NotNullValidator]string name)
    {
        Console.WriteLine(PrepareName(name));
    }

    private string PrepareName(string name)
    {
        throw new NotImplementedException();
    }
}

public class Program
{
    private static void Main(string[] argv)
    {
        var logic = PolicyInjection.Create<Logic>();
        logic.SayMyName("Kuba");
    }
}
Listing 3. Podróż wyjątku.

dostajemy następujący efekt:

image

Wygląda to, jakby wyjątek powstał gdzieś pomiędzy wywołaniem, a samym ciałem metody (co jest poniekąd prawdą). Spodziewalibyśmy się okienka dokładnie w miejscu wyrzucenia, czyli w metodzie PrepareName(). Na szczęście jest jeszcze stack trace, który zawiera cały stos wraz z miejscem postania wyjątku. Niby nic, ale każdy mniej doświadczony programista zgłosi się do nas, uznając, że jest to błąd w architekturze.

Z czego takie zachowanie wynika? Warto prześledzieć, co dzieje się z wyjątkiem:

  1. Wyjątek jest rzucany w metodzie PrepareName().
  2. Jako, że metoda SayMyName została wywołana za pomocą infrastruktury PIAB (poprzez refleksję - MethodBase.Invoke()), więc zostanie on opakowany przez TargetInvocationException. Ten zostanie przechwycony (listing 2, linijki 6-9), oryginalny wyjątek zostanie wyłuskany i przekazany jako element wyniku działania funkcji do wcześniejszych handlerów.
  3. Gdy wróci przez wszystkie handlery, to zostaje ponownie wyrzucony.

Stąd właśnie inne miejsce wyrzucenia wyjątku, wskazywane przez okienko "unhandled exception".

Prawdziwe problemy - środowisko hostowane, czyli partial trust...

Swojego czasu musiałem umieścić aplikację na jednym z większych polskich serwisów prowadzących hosting Windows. I tutaj zaczęły się prawdziwe schody. Problem polegał na tym, że w takich środowiskach poziom zabezpieczeń prawie nigdy nie jest ustawiony na Full Trust, a Enterprise Library nie zawsze jest dostępny z GAC (tak było w moim przypadku). Od razu zaznaczam, że problemem nie był fakt, że dopiero wersja 4.0 ma atrybut AllowPartiallyTrustedCallersAttribute (tzw. APTCA), a więc wersje wcześniejsze trzeba przekompilować bez podpisu cyfrowego, aby w ogóle się uruchomiły.

Problem polega na tym, że PIAB od środka wykorzystuje własne rzeczywiste proxy - InterceptingRealProxy, które dziedziczy po System.Runtime.Remoting.Proxies.RealProxy, która to klasa ma jako jeden z atrybutów:

[SecurityPermission(SecurityAction.InheritanceDemand, Flags=SecurityPermissionFlag.Infrastructure)]

Oznacza to, że aby dziedziczyć po RealProxy, assembly musi mieć uprawnienie do wpinania się do infrastruktury .NET Remoting. Jak nie trudno zgadnąć, we współdzielonym środowisku hostowanym zwykle takiego uprawnienia nie dostaniemy, a więc próba utworzenia proxy skończy się... SecurityException. Możemy poprosić administratora o nadanie tego uprawnienia, choć jest mało prawdopodobne, że się zgodzi.

Sytuacja na szczęście nie jest patowa - istnieje jeszcze jedno wyjście. PIAB pozwala na napisanie i podpięcie własnego Policy Injectora, który już z infrastruktury .NET Remoting korzystać nie musi. Na takie wyjście ja się zdecydowałem - postanowiłem napisać mechanizm, który będzie dynamicznie generował obiekty proxy implementujące wskazany interfejs i dalej wywołujące całą infrastrukturę Policy Injection (oczywiście takie podejście wyklucza wstrzykiwanie polityk do MarshalByRefObject). I po kilka dniach wspaniałej przygody z dynamicznym generowaniem kodu IL, z której wrażenia można porównać jedynie do czyszczenia zatkanych rur kanalizacyjnych, uzyskałem w miarę zadowalające rozwiązanie.

Pozwolę sobie nie przedstawiać dalszych szczegółów technicznych, gdyż nikt i tak by tego nie przeczytał, a najwięksi śmiałkowie pewnie zasnęliby w trakcie. Niemniej jednak, gdy moje rozwiązanie dopracuję (ma jeszcze kilka znanych mi błędów), to postaram się je opublikować, aby więcej osób mogło skorzystać z moich walk i nie musiało samemu przechodzić przez to piekiełko :).

Podsumowanie

Policy Injection Application Block to całkiem interesujące i wygodnie rozwiązanie, niemniej jednak, strategia przechwytywania wywołań wybrana przez twórców (infrastruktura .NET Remoting) może powodować problemy przy pewnych specyficznych sytuacjach, jak np. wdrażanie w środowisku o mniejszych uprawnieniach. Warto zdawać sobie z tego sprawę przy wyborze technologii wspierającej AOP podczas prac nad architekturą aplikacji.

Opublikowane 31 lipca 2008 01:01 przez jakubin

Komentarze:

# Przygody z Policy Injection Application Block

Policy Injection Application Block w skr&#243;cie to biblioteka pozwalająca na wstrzyknięcie kodu opakowującego

31 lipca 2008 01:55 by Zine.NET

# re: Przygody z Policy Injection Application Block

Kiedy słyszę o PIAB, zawsze przypomina mi się ten tekst:

http://blogs.msdn.com/edjez/archive/2007/02/23/policy-injection-app-block-behind-the-scenes.aspx

który mogę śmiało polecić jako lekturę dodatkową do wpisu Kuby. W artykule Edward Jezierski dokonuje przeglądu dostępnych w .NET mechanizmów umożliwiających przechwytywanie wywołań metod i krótko opisuje zalety i wady każdego z nich.

Miłej lektury.

31 lipca 2008 03:40 by apl

# re: Przygody z Policy Injection Application Block

@apl: Właśnie tego linka szukałem od jakiegoś czasu. Dzięki :).

31 lipca 2008 08:53 by jakubin

# re: Przygody z Policy Injection Application Block

Interesting! Bardzo chętnie zaciągnę i poprzeglądam sobie kod który zapowiedziałeś udostępnić, coby wczuć się w twe cierpienia:). Nasz Werterze.

31 lipca 2008 15:06 by Procent

# Przygody z Policy Injection Application Block

Dziękujemy za publikację - Trackback z dotnetomaniak.pl

10 listopada 2009 12:49 by dotnetomaniak.pl
Komentarze anonimowe wyłączone

About jakubin

MVP w kategorii C#, MCP. Aktualnie pracuje w Webstruments.pl jako programista C#.