Konkurs Enterprise Library – Policy Injection (PIAB) - Updated

Oto ostatnia odsłona konkursu. Tym razem wprowadzenie do wykorzystania PIAB przy scalaniu poszczególnych rozwiązań opartych na LAB, EHAB i VAB.

Refaktoryzacja do Policy Injection Application Block (PIAB)

Jeżeli podczas projektowania aplikacji zdecydowaliśmy się na zastosowanie pewnych wytycznych czy standardów implementacji, wówczas spora część kodu wynikająca z tych ustaleń jest powtarzalna. Niestety, jeśli piszemy coś ręcznie istnieje możliwość popełnienia błędu. Aby uchronić się przed pomyłkami można napisać rozszerzenia lub makra Visual Studio pomagające nam we wdrażaniu standardów, niestety to zadanie nie należy do najłatwiejszych. Innym sposobem jest użycie narzędzia pozwalającego na zastosowanie elementów programowania aspektowego. Takimi aspektami mogą być na przykład logowanie, obsługa błędów czy walidacja. PIAB pozwala na dodawanie aspektów do nowych lub istniejących obiektów, dzięki wytwarzaniu obiektów proxy realizujących wymaganą funkcjonalność.

Przypadek

Walidacja, obsługa wyjątków oraz logowanie używają Application Block, lecz brak spójnych reguł ich użycia.

Rozwiązanie

Zlokalizuj fragmenty kodu z walidacją, logowaniem i obsługą wyjątków. Podziel poszczególne przypadki użycia, według sposobu przetwarzania i utwórz reguły ich dotyczące. Zaaplikuj reguły do istniejącego kodu.

Uzasadnienie

Co nam daje PIAB? Upraszcza kod aplikacji po przez automatyzowanie pewnych operacji w kodzie oraz zmniejsza ryzyko popełnienia błędu w przypadku ręcznego pisania kodu operacji.

Załóżmy, że celem logowania jest obsługa pewnych kroków scenariusza działania aplikacji – dla przykładu reguła mówi loguj każde wywołanie operacji obiektów warstwy usług lub każda zmiana wartości modelu danych powinna być logowana dla celów informacyjnych. Implementacja powyższych reguł może wyglądać tak, że dodawane są instrukcje logujące na początku i końcu działania metody. Ręczne wytwarzanie takiego kodu jest narażone na błędy oraz trudności związane ze zmianą reguł. W takim przypadku warto jest użyć jakiegoś automatu który wykona wymagane instrukcje, takim narzędziem jest PIAB.

Podobnie można postąpić w przypadku gdy obsługa wyjątków ma na celu realizację reguł typu: osłoń wyjątek bazy danych po przez zastąpienie go innym na poziomie warstwy usług lub ukryj każdy wyjątek na poziomie GUI, jednocześnie logując informacje o nim. Po zdefiniowaniu zasad obsługi reguł i wykorzystaniu istniejącej konfiguracji Application Block, PIAB automatycznie wykonywał będzie wskazane reguły.

W przypadku walidacji również można uprościć kod aplikacji gdy posiadamy spójny zestaw reguł walidowania. PIAB wspomoże również obsługę reguł postaci Każdy parametr metod warstwy usług musi być poprawny.

Warto dodać iż PIAB wykorzystuje istniejącą konfigurację VAB, EHAB i LAB dodając tylko same zasady ich aplikowania.

Sposób wykonania

Mając aplikację używającą poszczególnych Application Block, pierwszym krokiem jest zastanowienie się nad regułami jakie chcemy zaimplementować – sposobami jak automatyzować operacje w aplikacji. Kolejnym krokiem jest ustalenie sposobu określenia wytycznych w konfiguracji PIAB. Na końcu usuwamy istniejący kod poszczególnych Application Block i zmieniamy sposób tworzenia obiektów podlegających modyfikacjom.

Podsumowując mamy następujące kroki:

  1. Określenie reguł,
  2. Konfiguracja PIAB – konfiguracja wytycznych,
  3. Usunięcie istniejącego kodu VAB, EHAB i LAB,
  4. Zmiana sposobu tworzenia obiektów.

Przykład

Cały proces aplikowania PIAB zaprezentowany zostanie przy użyciu przykładowej aplikacji. Projekt ZineNet.PiabContest.Base to aplikacja 3 warstwowa używająca VAB, EHAB i LAB.

Kontrakt kodu usług bazuje na interfejsach – to znaczy funkcjonalność API usług jest definiowana za pomocą interfejsów. Tworzeniem instancji usług zajmuje się klasa ServiceFactory.

namespace ZineNet.PiabContest.Services {
  public interface IFooService {
    FooEntity GetById(Guid id);
    List<FooEntity> GetByNameStart(string namePart);
    FooEntity Set(FooEntity value);
    void Remove(Guid id);
  }
}
namespace ZineNet.PiabContest.Model {
  [HasSelfValidation]
  public sealed class FooEntity {
    public FooEntity() { _id = Guid.NewGuid(); }
    public FooEntity(Guid id, string name, DateTime from, DateTime to) {
      Id = id;
      Name = name;
      ValidFrom = from;
      ValidTo = to;
    }
    public Guid Id { get ...
      set {
        if (value == Guid.Empty)
        { throw new ArgumentException("FooEntity's Id attribute cannot be empty.", "value"); }
        _id = value;
        Logger.Write("Set FooEntity.Id", "General");
      }
    }
    [NotNullValidator]
    [RegexValidator(@"^ [A-Z] [a-zA-Z0-9-_\.]{3,15} $", RegexOptions.IgnorePatternWhitespace)]
    [StringLengthValidator(4, RangeBoundaryType.Inclusive, 16, RangeBoundaryType.Inclusive)]
nbsp;   public string Name { ... }
    [PropertyComparisonValidator("ValidTo", ComparisonOperator.LessThanEqual)]
    public DateTime ValidFrom { ... }
    [PropertyComparisonValidator("ValidFrom", ComparisonOperator.GreaterThanEqual)]
    public DateTime ValidTo { ... }
    [SelfValidation]
    public void ValidateThis(ValidationResults results) {
      if (_id == Guid.Empty) { ... }
      if (_from.Date != _from) { ... }
      if (_to.Date != _to) { ... }
    }
  }
}

Teoretycznie aplikacja składa się z warstwy dostępu do danych, na której działa warstwa usług. W celu uproszczenia przykładu, baza danych została zastąpiona statycznym słownikiem na obiekty FooEntity. Błędy dostępu do bazy danych symulują wyjątki pochodne klasy SystemException.

namespace ZineNet.PiabContest.Services {
  sealed class FooService: IFooService {
    private static Dictionary<Guid, FooEntity> _data = ...;
    public FooEntity GetById(Guid id) {
      FooEntity e = null;
      Logger.Write("Before FooService.GetById", "General");
      try {
        if (id == Guid.Empty) { throw new FakeSqlException("Id column of Foo table cannot be empty."); }
        e = _data[id];
      } catch (SystemException ex) {
        if (ExceptionPolicy.HandleException(ex, "WrapSystemExPolicy")) {
          throw;
        }
      }
      Logger.Write("After FooService.GetById", "General");
      return e;
    }
    public List<FooEntity> GetByNameStart(string namePart) {
      Logger.Write("Before FooService.GetByNameStart", "General");
      ...
      Logger.Write("After FooService.GetByNameStart", "General");
      return result;
    }
    public FooEntity Set(FooEntity value) {
      try {
        ValidationResults r = Validation.Validate(value);
        if (!r.IsValid) { throw new FakeSqlException("FooEntity validation failed."); }
      } catch (SystemException ex) {
        if (ExceptionPolicy.HandleException(ex, "WrapSystemExPolicy")) {
          throw;
        }
      }
      Logger.Write("Before FooService.Set", "General");
      ...
      Logger.Write("After FooService.Set", "General");
    }
    public void Remove(Guid id) {
      Logger.Write("Before FooService.Remove", "General");
      ...
      Logger.Write("After FooService.Remove", "General");
    }
    private sealed class FakeSqlException: SystemException {
      public FakeSqlException(string message)
        : base("SQL Database Error: " + message) {}
    }
  }
}

Konsumentem usług, a zarazem symulacją warstwy interfejsu użytkownika zajmuje się klasa EntryPoint.

namespace ZineNet.PiabContest {
  static class EntryPoint {
    static void Main() {
      QuickTests();
      try {
        ExCallService();
      } catch (Exception ex) {
        Console.WriteLine(" [EXCEPTION] " + ex.Message);
      }
      try {
        ExCallServiceSet();
      } catch (Exception ex) {
        Console.WriteLine(" [EXCEPTION] " + ex.Message);
      }
      try {
        ExModifyEntity();
      } catch (Exception ex) {
        Console.WriteLine(" [EXCEPTION] " + ex.Message);
      }
      Console.ReadKey();
    }
    private static void QuickTests() { ... }
    private static void ExCallService() { ... }
    private static void ExModifyEntity() { ... }
  }
}

Użycie poszczególnych Application Block polega na:

  1. VAB – walidacja stanu obiektów FooEntity – modelu danych.
  2. LAB – logowanie zmian obiektów modelu danych oraz wywołań metod usług.
  3. EHAB – osłona wyjątków z warstwy dostępu do danych oraz ukrycie szczegółów wyjątków przed użytkownikiem.

Diagram klas przykładowej aplikacji

Diagram klas przykładowej aplikacji

Definiowanie reguł i wytycznych

Reguły określone przez architekturę systemu to:

  1. Logowanie wywołań metod warstwy usług
  2. Logowanie zmian stanu modelu danych
  3. Osłona wyjątków warstwy dostępu do danych (symulowanych przez wyjątki pochodne SystemException)
  4. Osłona wyjątków warstwy usług
  5. Osłona wyjątków modelu danych.
  6. Walidacja stanu modelu danych przekazywanych do warstwy usług

Ponieważ interfejsy usług znajdują sie w jednej przestrzeni nazw wraz z innymi typami danych, możemy zdefiniować następującą wytyczną:

A. Każda metoda z każdego typu CLR danych o nazwie IFooService jest interesująca

Zmiana stanu modelu danych może być monitorowana po przez przechwycenie wywołania metody set na odpowiedniej właściwości. Oznaczamy każdą metodę set właściwości nas interesującej tagiem o nazwie ZineNet.PiabContest. Definiujemy następującą wytyczną:

B. Każda metoda oznaczona tagiem o nazwie ZineNet.PiabContest jest interesująca.

Reguły 3, 4 i 6 definiują również poprzednią wytyczną - A.

Reguła 5 definiuje poprzednią wytyczną – tym razem B.

Konfiguracja PIAB

Edytujemy plik konfiguracyjny przy użyciu znanego narzędzia Enterprise Library Configuration Editor.

  1. Dodajemy Policy Injection Application Block
  2. W nim tworzymy dwie wytyczne (Policy) o nazwach ByTagPolicy i ServicePolicy
    Widok ogólny na konfigurację
  3. Do wytycznej ByTagPolicy dodajemy regułę dopasowania według TagAttribute (Tag Attribute Matching Rule) i ustawiamy wartości następująco:
    Ustawienia dla wytycznej ByTagPolicy 1
  4. Do wytycznej ServicePolicy dodajemy regułę dopasowania według typu CLR (Type Matching Rule) i ustawiamy właściwości następująco:
    Ustawienia dla wytycznej ServicePolicy 1
    Matches:
    Ustawienia dla wytycznej ServicePolicy 2

Kolejnym krokiem jest dodanie rodzaju obsługi (Handlers) do wytycznych

  1. Do wytycznej ByTagPolicy dodajemy obsługę logowania (*.Log) i wyjątków (*.Exception) – potrzebne dla reguł 2 i 5. Ustawiamy je w następujący sposób:
    Ustawienia dla wytycznej ByTagPolicy 2

    Ustawienia dla wytycznej ByTagPolicy 3
  2. Do wytycznej ServicePolicy dodajemy obsługę logowania (*.Log), wyjątków (*.Exception) i walidacji (*.Validation) – potrzebne dla reguł 1, 3, 4 i 6. Ustawiamy je w następujący sposób:
    Ustawienia dla wytycznej ServicePolicy 3

    Ustawienia dla wytycznej ServicePolicy 4

    Ustawienia dla wytycznej ServicePolicy 5

Usunięcie istniejącego kodu Application Block

Usuwamy kod walidacji, logowania i przechwytywania wyjątków z klas FooService, FooEntity oraz EntryPoint.

public FooEntity GetById(Guid id) {
  FooEntity e = null;
  Logger.Write("Before FooService.GetById", "General");
  try {
    if (id == Guid.Empty) { throw new FakeSqlException("Id column of Foo table cannot be
empty."
); }
      e = _data[id];
  } catch (SystemException ex) {
    if (ExceptionPolicy.HandleException(ex, "WrapSystemExPolicy")) {
      throw;
    }
  }
  Logger.Write("After FooService.GetById", "General");
  return e;
}
...
public string Name {
    get { return _name; }
    set {
    _name = value;
    Logger.Write("Set FooEntity.Name", "General");
  }
}

Po modyfikacji

public FooEntity GetById(Guid id) {
  if (id == Guid.Empty) { throw new FakeSqlException("Id column of Foo table cannot be empty."); }
  return _data[id];
}
...
public string Name { get ... [Tag("ZineNet.PiabContest")] set ... }

Dopasowania

Bazując na prostej aplikacji przedstawię sposób użycia kilku podstawowych dopasowań.

Aplikacja ma zdefiniowane 2 typy danych, które będą podlegały zmianom przez PIAB.

Typy danych podlegające modyfikacjom PIAB

namespace ZineNet.PiabContest.MyMatches {
  namespace SubA {
    public sealed class Aaa : MarshalByRefObject {
      ...
      public string Name {
        [Tag("myGet")] get { return _name; }
        [Tag("mySet")] set { _name = value; }
      }
    }
    public sealed class Aaa_111 : MarshalByRefObject {
      ...
      public string Name {
        [Tag("myGet")] get { return _name; }
        [Tag("mySet")] set { _name = value; }
      }
    }
  }
  namespace SubB {
    public sealed class Bbb : MarshalByRefObject {
      ...
      public string Name {
        [Tag("myGet")] get { return _name; }
        [Tag("mySet")] set { _name = value; }
      }
    }
    public sealed class Bbb_111 : MarshalByRefObject {
      ...
      public string Name {
        [Tag("myGet")] get { return _name; }
        [Tag("mySet")] set { _name = value; }
      }
    }
  }
}

Testowa metoda tworzy instancję każdego z typów za pomocą klasy PolicyInjection. Następnie na każdym obiekcie wykonywana jest operacja get/set:

o.Name = o.Name;

Podczas tworzenia obiektu przekazywana jest dodatkowo konfiguracja PIAB wczytana z pliku App_*.config znajdującego w katalogu aplikacji. Dzięki temu można stworzyć, bądź edytować plik konfiguracyjny i obserwować zmiany bez potrzeby rekompilacji aplikacji.

Podstawowy plik konfiguracyjny ma zdefiniowane logowanie na konsole aplikacji.

By Assembly

Konfiguracja

<matchingRules>
  <add name="Assembly Matching Rule"type="...AssemblyMatchingRule..."
       match="ZineNet.PiabContest.MyMatches" />
</matchingRules>

Wynik

  SubA.Class1.get_Name #
  SubA.Class1.set_Name # value - A_Class1;
  SubA.Class2.get_Name #
  SubA.Class2.set_Name # value - A_Class2;
  SubB.Class1.get_Name #
  SubB.Class1.set_Name # value - B_Class1;
  SubB.Class2.get_Name #
  SubB.Class2.set_Name # value - B_Class2;

Parametrem reguły jest ZineNet.PiabContest.MyMatches czyli każdy typ tworzony/owijany przez PIAB będzie logowany.

By MemberName

Konfiguracja

<matchingRules>
  <add name="Member Name Matching Rule" type="...MemberNameMatchingRule..." >
    <matches>
      <add match="get_*" ignoreCase="false" />
    </matches>
  </add>
</matchingRules>

Wynik

SubA.Class1.get_Name #
  SubA.Class2.get_Name #
  SubB.Class1.get_Name #
  SubB.Class2.get_Name #

Parametrem reguły jest get_* czyli każda metoda zaczynająca się od get_ przez PIAB będzie logowana. Właściwości C# tłumaczone są przez kompilator na metody odpowiednio get_ lub set_.

By Namespace

Konfiguracja

<matchingRules>
  <add name="Namespace Matching Rule" type="...NamespaceMatchingRule...">
    <matches>
      <add match="ZineNet.PiabContest.MyMatches.SubA" ignoreCase="false" />
    </matches>
  </add>
</matchingRules>

Wynik

SubA.Class1.get_Name #
  SubA.Class1.set_Name # value - A_Class1;
  SubA.Class2.get_Name #
  SubA.Class2.set_Name # value - A_Class2;

Parametrem reguły jest ZineNet.PiabContest.MyMatches.SubA czyli każdy typ tworzony/owijany przez PIAB z tej przestrzeni nazw będzie logowany.

By Property

Konfiguracja

<matchingRules>
  <add name="Property Matching Rule" type="...PropertyMatchingRule..." >
    <matches>
      <add match="Name" ignoreCase="false" />
    </matches>
  </add>
</matchingRules>

Wynik

SubA.Class1.get_Name #
  SubA.Class1.set_Name # value - A_Class1;
  SubA.Class2.get_Name #
  SubA.Class2.set_Name # value - A_Class2;
  SubB.Class1.get_Name #
  SubB.Class1.set_Name # value - B_Class1;
  SubB.Class2.get_Name #
  SubB.Class2.set_Name # value - B_Class2;

Parametrem reguły jest Name czyli każda właściwość o tej nazwie będzie przez PIAB będzie logowana.

By TagAttribute

Konfiguracja

<matchingRules>
  <add name="Tag Attribute Matching Rule"
       match="myGet" ignoreCase="false" type="...TagAttributeMatchingRule..."/>
</matchingRules>

Wynik

SubA.Class1.get_Name #
  SubA.Class2.get_Name #
  SubB.Class1.get_Name #
  SubB.Class2.get_Name #

Parametrem reguły jest myGet czyli każdy element posiadający atrybut [Tag] o wartości myGet będzie przez PIAB logowany. W tym przypadku będą to metody get właściwości.

By TypeName

Konfiguracja

<matchingRules>
  <add name="Type Matching Rule" type="...TypeMatchingRule..." >
    <matches>
      <add match="Class1" ignoreCase="false" />
    </matches>
  </add>
</matchingRules>

Wynik

SubA.Class1.get_Name #
  SubA.Class1.set_Name # value - A_Class1;
  SubB.Class1.get_Name #
  SubB.Class1.set_Name # value - B_Class1;

Parametrem reguły jest Class1 czyli każdy element typu o tej nazwie będzie przez PIAB logowany. W tym przypadku będą to metody get właściwości.

Complex

Dopasowania można łączyć za pomocą operatora AND, po prostu dodając wiele elementów do węzła Matches. Bardziej złożone warunki można tworzyć przy użyciu projektu EntLibContrib, lub za pomocą własnych dopasowań.

Konfiguracja

<matchingRules>
  <add name="Member Name Matching Rule" type="...MemberNameMatchingRule...">
    <matches>
      <add match="get_*" ignoreCase="false" />
    </matches>
  </add>
  <add name="Namespace Matching Rule" type="...NamespaceMatchingRule...">
    <matches>
      <add match="ZineNet.PiabContest.MyMatches.SubB" ignoreCase="false" />
    </matches>
  </add>
  <add name="Type Matching Rule" type="...TypeMatchingRule...">
    <matches>
      <add match="Class2" ignoreCase="false" />
    </matches>
  </add>
</matchingRules>

Wynik

SubB.Class2.get_Name #

W tym przypadku logowaniu poddane zostaną typy z przestrzeni nazw ...SubB o nazwie Class2 i to tylko metody o nazwie zaczynającej sie od get_.

Własny Handler i Matching Rule

Własny Handler będzie wypisywał tylko linijkę tekstu na konsolę, przed i po wywołaniu metody.

namespace ZineNet.PiabContest.MyMatches {
  [ConfigurationElementType(typeof(CustomMatchingRuleData))]
  public sealed class MyCutomMatching : IMatchingRule {
    public MyCutomMatching(NameValueCollection notUsed) {}
    public bool Matches(MethodBase member) {
      bool nsMatches = member.DeclaringType.Namespace == typeof(SubA.Class2).Namespace;
      bool typeMatches = member.DeclaringType.Name == typeof(SubA.Class2).Name;
      bool nameMatches = member.Name == "set_Name";
      return nsMatches && (typeMatches || nameMatches);
    }
  }
  [ConfigurationElementType(typeof (CustomCallHandlerData))]
  public class MyCustomHandler: ICallHandler {
    public MyCustomHandler(NameValueCollection notUsed) { }
    public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext) {
      Console.WriteLine("{");
      IMethodReturn msg = getNext()(input, getNext);
      Console.WriteLine("}");
      return msg;
    }
  }
}

Konfiguracja

<matchingRules>
  <add name="Custom Matching Rule"
       type="ZineNet.PiabContest.MyMatches.MyCutomMatching, ZineNet.PiabContest.MyMatches"
  />
</matchingRules>
<handlers>
  <add name="Custom Handler"
       type="ZineNet.PiabContest.MyMatches.MyCustomHandler, ZineNet.PiabContest.MyMatches"
  />
  ...
</handlers>

Wynik

{
  SubA.Class1.set_Name # value - A_Class1;
}
{
  SubA.Class2.get_Name #
}
{
  SubA.Class2.set_Name # value - A_Class2;
}

Reguła dopasowania określa każdy typ z przestrzeni nazw SubA, którego nazwa to Class2 lub metoda/właściwość set_Name.

Zadanie konkursowe #4 – Policy Injection Application Block, Enterprise Library 3.1

Zgodnie z założeniami konkursu na koniec zadania do wykonania.

Zgodnie z opisanymi powyżej krokami zrefaktoryzować kod aplikacji konkursowej tak, aby wyeliminować jak najwięcej powtarzalnego kodu logowania, obsługi błędów i walidacji. W szczególności:

  1. Sposób logowania zgodny z zadaniem konkursowym #1 oraz:
    • Warstwa usług powinna logować wszystkie swoje operacje – zarówno te z IDataService{T} jak i interfejsów pochodnych.
    •  Powinna logować sukces (po wykonaniu kodu).
  2. Sposób obsługi błędów zgodny z zadaniem konkursowym #2
    • Każda z operacji usług powinna chronić wyjątki generowane przez warstwy niższe jak i samą siebie.
    •  Należy użyć wyjątku specjalnie dedykowanego operacjom na usługach..
    • Warstwa prezentacji powinna mieć wyciszanie wyjątków dla każdego handlera *Click lub *DoubleClick.
    •  Zawartość błędu powinna być logowana (z warstwy prezentacji oraz usług) oraz ogólna wiadomość wyświetlona użytkownikowi.
  3. Sposób walidacji zgodny z zadaniem konkursowym #3
    • Jeżeli obiekt posiadający zdefiniowane reguły walidacji jest przekazywany do usługi, musi być poprawny.

Ponieważ realizacja tych założeń jest zależna od poprzednich konkursów, więc można wysłać rozwiązanie na każdy z trzech punktów (LAB, EHAB, WAB). Rozwiązanie każdego z powyższych punktów to osobna szansa na wygraną, warto więc pokusić się o wszystkie 3 punkty i 3 razy trafić do 'szczęśliwego kapelusza'.

Rozwiązane zadania należy przesyłać na adres mgrzeg at gmail kropka com. Reguły wiadomości:

Temat wiadomości: [ENTLIB PIAB] Rozwiązanie zadania konkursowego;

Treść (może być w załączonym pliku .txt, .doc, .rtf) powinna zawierać krótki opis co zostało wykonane;

Plik .zip zawierający kod aplikacji konkursowej po refaktoryzacji. Można dołączyć również kod wykonywalny (bez bibliotek EntLib, etc. - tylko to, co niezbędne);

Dane adresowe do wysyłki nagrody - NIE! Skontaktujemy się ze "szczęśliwcem" via email i ustalimy szczegóły.

Adresy:

Kod aplikacji przykładowej opisywanej w tekście

Kod aplikacji konkursowej

UPDATE: Ostateczny termin przysyłania rozwiązań to 5 listopada 2007 roku.

W razie wątpliwości etc. proszę zadawać pytania.

Powodzenia!

Opublikowane 15 października 07 04:40 przez Wojciech Gebczyk

Komentarze:

Brak komentarzy
Komentarze anonimowe wyłączone

About Wojciech Gebczyk

Code Sculptor.