Projektowanie, Programowanie, Codzienność – BeniaminZaborski.com

11 stycznia 2015

Jak new ProxyGenerator() z Castle.DynamicProxy może zabić maszynę

Filed under: How Can I Take ... — Tagi: , , — Beniamin Zaborski @ 18:55

Castle.DynamicProxy to fajna biblioteka do dynamicznego generowania obiektów proxy. Zwykle obiekt proxy przydaje się nam gdy chcemy wykonać dodatkową operację przed, po czy na wystąpienie błędu wykonania właściwej metody. Najbardziej trywialny przykład to logowanie błędów, ale można wyobrazić sobie także bardziej wysublimowane przykłady jak choćby zarządzanie transakcjami.
Przykładowy kod tworzący obiekt proxy dla naszego obiektu implementującego interfejs ISomeBusinessComponent (z metodą DoMagic) i aplikujący jeden prosty interceptor SomeInterceptor mógłby wyglądać tak:

ProxyGenerator proxyGenerator = new ProxyGenerator();
var componentProxy = proxyGenerator.CreateInterfaceProxyWithTarget(component, new SomeInterceptor());
componentProxy.DoMagic();

Pierwszy krok to utworzenie instancji ProxyGenerator. Następnie możemy wygenerować obiekt proxy i wykonać na nim metodę DoMagic. Nic wyszukanego – prosty przykład. Ale co jeśli chcielibyśmy intensywnie i w wielu miejscach aplikacji tworzyć obiekty proxy. Hmm … po prostu powyższy kod wstawilibyśmy w te miejsca gdzie powinien być wywołany. Co sprytniejsi ubraliby go nawet w jakąś metodę utilową. Zasymulujmy taki przypadek za pomocą pętli:

foreach (var component in components)
{
  ProxyGenerator proxyGenerator = new ProxyGenerator();
  var componentProxy = proxyGenerator.CreateInterfaceProxyWithTarget(component, new SomeInterceptor());
  componentProxy.DoMagic();
}

Kod na pierwszy rzut oka może i wygląda OK (wiem można przenieść instancjonowanie ProxyGenerator poza pętlę – to symulacja). W rzeczywistości przy każdym wywołaniu przebiegu pętli będziemy tworzyć nową instancję ProxyGenerator, a jest to kosztowna operacja jeśli chodzi o pamięć (tworzenie m.in. Assembly).
Rozwiązanie? W tej symulacji po prostu należałoby wynieść tworzenie obiektu proxyGenerator poza pętlę. Inaczej mówiąc prosi się tu o singleton dla ProxyGeneratora.

foreach (var component in components)
{
  ProxyGenerator proxyGenerator = ProxyGeneratorSingleton.Instance;
  var componentProxy = proxyGenerator.CreateInterfaceProxyWithTarget(component, new SomeInterceptor());
  componentProxy.DoMagic();
}

Odpowiednio częste wykonywanie tworzenia instancji ProxyGenerator potrafi skutecznie skonsumować całą dostępną pamieć operacyjną na naszej maszynie.

Działający przykład zamieszczam tutaj.

Reklamy

28 grudnia 2014

SQL Server – optymalne rozmieszczenie plików

Filed under: How Can I Take ... — Tagi: , , , — Beniamin Zaborski @ 22:10

Utworzenie nowej bazy danych w SQL Server zapewni nam poniższe polecenie języka T-SQL:

CREATE DATABASE ENT_PROJECT
GO

Co tak naprawdę się stanie po wykonaniu tego polecenia? Zanim coś więcej o tym, to wcześniej należy wspomnieć o sposobie przechowywania danych na dysku przez SQL Server.

SQL Server mapuje bazy danych na zestaw plików przechowywanych w systemie plików systemu operacyjnego. Zapisywane są dwa rodzaje danych: właściwe dane oraz log transakcji. Dane i log nigdy nie są mieszane w jednym pliku – zawsze znajdują się w osobnych plikach. Jeden plik zawsze odnosi się tylko do jednej bazy danych – nigdy nie zawiera danych czy informacji z kilku baz.

SQL Server posiada zasadniczo trzy typy plików:

Primary data files – zawiera dane oraz wskazania na inne pliki bazy danych, istnieje tylko jeden taki plik na bazę danych, posiada rozszerzenie mdf.
Secondary data files – zawiera dane, może istnieć wiele plików tego typu na bazę danych, posiada rozszerzenie ndf.
Log files – zawiera log transakcji potrzebny do odtworzenia bazy danych, musi być przynajmniej jeden taki plik na bazę danych ale może być ich więcej, posiada rozszerzenie ldf.

Wskazane rozszerzania plików są jedynie rozszerzeniami sugerowanymi i możemy użyć innych jeśli mamy taką potrzebę. Oprócz samych plików SQL Server wprowadza także pojęcie filegroups, które są nazwanymi grupami plików i są tworzone w celu ułatwienia zarządzania bazą danych. Możemy wyróżnić dwa rodzaje grup plików:

Primary – zawiera pliki typu primary data files
User-definied – opcjonalnie utworzone przez użytkownika, zawierają dodatkowe pliki danych utworzone przez użytkownika.

Grupy plików zawierają a raczej grupują jedynie pliki danych. Pliki logów transakcji nie są przechowywane w grupach plików. Dodać należy, że w grupie Primary będą przechowywane wszystkie obiekty systemowe bazy danych (oczywiście pośrednio, gdyż de facto znajdują się one w plikach danych).

Wróćmy do przykładu polecenia T-SQL tworzącego bazę danych. Nie wyspecyfikowałem w nim żadnych informacji odnośnie plików czy grup plików podczas tworzenia bazy danych, więc został przyjęty model domyślny. Model domyślny oznacza jedną grupę plików PRIMARY z jednym plikiem danych oraz jeden plik logów (oczywiście poza jakąkolwiek grupą). Wszystkie moje dane jak i dane systemowe będą przechowywane w jednym pliku danych – primary.

W niewielkich bazach danych czy w bazach które nie są zbyt intensywnie wykorzystywane (niewielka ilość transakcji na sekundę) taka konfiguracja może być OK. Co innego gdy mamy do czynienia z dużym dedykowanym systemem klasy enterprise. W takim wypadku wskazane jest utworzyć kilka grup plików. Nasuwają się pytania: ile utworzyć grup plików, jak je optymalnie rozmieścić? Istnieje kilka dobrych praktyk i postaram się te znane mi przytoczyć.

Po pierwsze musimy być świadomi ilości dysków/kontrolerów dyskowych naszego serwera, a także ilości rdzeni CPU. Jeśli mamy już taką wiedzę weźmy pod uwagę poniższe:

– Pliki logów powinny być na osobnym dysku, szybkim dysku, gdyż do nich odbywa się intensywny zapis. Dostęp do plików logów jest sekwencyjny więc nie ma większego sensu tworzyć więcej niż jednego pliku logów. Jedynym powodem do tego może być sytuacja gdy skończy się przestrzeń na dysku.

– Warto utworzyć tyle plików danych/grup plików ile dysków pomniejszając liczbę o dyski na pliki logów (w najprostszym przypadku o 1).

– Dobrym pomysłem jest odseparowanie danych systemowych od całej reszty. Uzyskamy to poprzez oznaczenie którejś z grup plików utworzonej przez użytkownika jako domyślnej (innej niż primary która standardowo jest domyślną).

– Pozytywnie na wydajność wpłynie utworzenie kilku plików dla bazy tempdb która jest intensywnie wykorzystywana. Jedna z zasad mówi, że plików danych powinno być tyle ile rdzeni CPU naszego serwera, a plików logów tyle ile fizycznych dysków (co stanowi wyjątek od tego co napisałem w punkcie pierwszym).

Utworzenie odpowiedniej ilości plików danych niejako determinuje nam konfiguracja sprzętowa, ale to tylko jedna strona medalu. Jeśli utworzyliśmy wiele grup plików to musimy odpowiednio rozmieścić nasze obiekty bazodanowe we właściwych grupach. Służy nam do tego słowo kluczowe ON filegroup. Zatem np. w poleceniu T-SQL do tworzenia tabeli CREATE TABLE możemy wskazać w której grupie plików nasza tabela będzie przechowywana. Jeśli nie wskażemy tej lokalizacji to będzie przyjęta domyślna czyli grupa plików z ustawionym znacznikiem DEFAULT. Optymalne rozmieszczenie tabel czy indeksów w określonych grupach plików zależy już mocno od specyfiki samego systemu jaki tworzymy. Na pewno nie jest to łatwe zadanie, ale kierować się można poniższymi wskazówkami:

– Warto myśleć w kategoriach zrównoleglenia wykonywanych operacji IO przez system bazodanowy versus obszary funkcjonalne naszej aplikacji.

– Dobrym pomysłem będzie umieszczenie indeksów tabeli jak i samej tabeli w różnych grupach plików. Inne podejście jest jeszcze bardziej radykalne i mówi o odseparowaniu od siebie także indeksów klastrowych i nieklastrowych.

 

Uzbrojeni w tę wiedzę, dysponując serwerem z pięcioma dyskami (C, D, E, F, G) i jednym cztero-rdzeniowym procesorem moglibyśmy utworzyć bazę danych np. w taki sposób:

CREATE DATABASE ENT_PROJECT
ON PRIMARY
(
NAME = 'ENT_PROJECT0_dat',
FILENAME = 'C:\DBs\ENT_PROJECT0_dat.mdf'
),
FILEGROUP G1
(
NAME = 'ENT_PROJECT1_dat',
FILENAME = 'E:\DBs\ENT_PROJECT1_dat.ndf'
),
FILEGROUP G2
(
NAME = 'ENT_PROJECT2_dat',
FILENAME = 'F:\DBs\ENT_PROJECT2_dat.ndf'
),
FILEGROUP G3
(
NAME = 'ENT_PROJECT3_dat',
FILENAME = 'G:\DBs\ENT_PROJECT3_dat.ndf'
)
LOG ON
(
NAME = 'ENT_PROJECT_log',
FILENAME = 'D:\DBs\ENT_PROJECT_log.ldf'
)
GO

ALTER DATABASE ENT_PROJECT
MODIFY FILEGROUP G1 DEFAULT
GO

ALTER DATABASE tempdb 
ADD FILE 
(
	NAME = 'tempdev1',
	FILENAME = 'D:\DBs\tempdb1.ndf'
),
(
	NAME = 'tempdev2',
	FILENAME = 'E:\DBs\tempdb2.ndf'
),
(
	NAME = 'tempdev3',
	FILENAME = 'F:\DBs\tempdb3.ndf'
)
GO

ALTER DATABASE tempdb 
ADD LOG FILE 
(
	NAME = 'templog1',
	FILENAME = 'D:\DBs\templog1.ldf'
),
(
	NAME = 'templog2',
	FILENAME = 'E:\DBs\templog2.ldf'
),
(
	NAME = 'templog3',
	FILENAME = 'F:\DBs\templog3.ldf'
),
(
	NAME = 'templog4',
	FILENAME = 'G:\DBs\templog4.ldf'
)
GO

Przykład zakłada że w każdej z grup istnieje tylko jeden plik danych. Jednakże spełnia nasze założenia, tj. każdy plik jest na osobnym dysku, grupa primary nie jest domyślną grupą, baza tempdb posiada kilka plików. Celowo pominąłem w przykładzie T-SQL kwestię rozmiaru plików bazy danych, aby skupić się tylko na samym rozmieszczeniu plików na dyskach. Nie oznacza, to że jest to mało istotna rzecz z punktu widzenia wydajności naszej bazy danych. Wręcz przeciwnie. Warto oszacować rozmiar naszej bazy i jej miesięczny przyrost. SQL Server potrafi zarezerwować w systemie plików wskazaną przestrzeń na dane by nie walczyć o nią gdy jej zabraknie. Z drugiej strony, dzięki funkcji autogrowth, SQL Server może powiększyć „zaalokowaną” przestrzeń dyskową gdy zacznie jej brakować. Należy jednak pamiętać, że to dość kosztowna operacja i nie powinna być wykonywana za często, gdyż wtedy potrafi rozłożyć całą maszynę na łopatki.

Teraz możemy utworzyć tabelę oraz indeks we wskazanej grupie plików:

CREATE TABLE USERS (
USER_ID UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
LOGIN VARCHAR(20) NOT NULL,
IS_ACTIVE BIT NOT NULL DEFAULT(1)
)
ON G2
GO

CREATE UNIQUE NONCLUSTERED INDEX IX_USERS_LOGIN
ON USERS(LOGIN ASC)
ON G3
GO

Jak widać tabela zostanie utworzona w grupie G2, a indeks nieklastrowy na tej tabeli zostanie utworzony w grupie G3.

Rozmieszczenie tabel w poszczególnych grupach plików należy szczególnie przemyśleć, gdyż po utworzeniu nie da się ich przenieść do innych grup plików.

Przedstawiłem kilka znanych mi reguł, ale pamiętajmy że są to jedynie suche reguły! Przed zaaplikowaniem ich na własnym środowisku należy spojrzeć na nie przez pryzmat własnego systemu.

19 grudnia 2014

Castle Windsor – Transient Memory Leak

Filed under: How Can I Take ... — Tagi: , , , , , — Beniamin Zaborski @ 22:20

Od jakiegoś czasu Castle Windsor to mój ulubiony kontener IoC. Jutro co prawda może się nim stać Ninject lub Autofac, ale nie dlatego że mi źle z Windsor, a raczej z czystej ciekawości. Przejdźmy do rzeczy.
Jak generalnie wyobrażamy sobie pracę z kontenerem IoC? Z reguły są dwa kroki do wykonania: rejestracja typu i pobranie instancji tego typu. W Windsorze w najprostszej formie wygląda to tak:

Rejestracja:

container.Register(
   Component.For<ISecurityService>()
   .ImplementedBy<SecurityService>()
);

Pobranie:

ISecurityService securityService = container.Resolve<ISecurityService>();

Niby wszystko jest OK i jest to jakaś alternatywa dla tego:

ISecurityService securityService = new SecurityService();

No tak, ale ktoś mógłby spytać o długość życia obiektu securityService. I w tym momencie dotykamy bardzo ważnej kwestii kontenerów IoC – LifeStyle. Każdy kontener IoC, Windsor także, posiada kilka wbudowanych elementarnych LifeStyle’i tj.: Singleton, Transient, Scope, PerWebRequest, itd.
LifeStyle określa zasięg obiektów instancjonowanych przez kontener oraz mówi kiedy będą niszczone. Singleton jest domyślnym lifestyle’m w Windsorze. Oznacza to, że będzie wytworzona jedna instancja przy pierwszym wywołaniu Resolve i reużywana przy kolejnych wywołaniach Resolve.
Zupełnym przeciwieństwem do Singletona jest Transient – przy każdym wywołaniu Resolve kontener zwraca nową instancję.

Rejestracja naszego serwisu dla Transient wyglądałaby tak:

container.Register(
   Component.For<ISecurityService>()
  .ImplementedBy<SecurityService>()
   .LifeStyle.Transient
);

Pobieramy tak zarejestrowany serwis, robimy z nim co mamy do zrobienia i zapominamy … Garbage Collector go zniszczy. Fałsz! Mina! Memory leak! Nic takiego nie nastąpi dopóki nie zniszczymy naszego kontenera IoC (jeśli w ogóle to robimy?). To jest dość często spotykana mina na którą się można nadziać. Windsor może trzymać referencję do obiektu transient, a więc Garbage Collector nie będzie w stanie zwolnić zasobów. Dla wszystkich obiektów transient najwyższego rzędu które zostaną przez nas jawnie pobrane z kontenera IoC musimy wykonać Release. Poprawna sekwencja powinna wyglądać tak:

Rejestracja:

container.Register(
   Component.For<ISecurityService>()
   .ImplementedBy<SecurityService>()
   .LifeStyle.Transient
);

Pobranie:

ISecurityService securityService = container.Resolve<ISecurityService>();

Praca z serwisem:

securityService.Authenticate(authDTO);

Zwolnienie:

container.Release(securityService);

I tak sobie myślę, że potencjalnie wbrew pozorom najbardziej zagrożeni nadzianiem się na tę minę nie są początkujący użytkownicy kontenerów IoC (jest szansa że oni uważnie czytają dokumentację), a raczej tacy którzy przesiadają się na Windsora z jakiś „starszych” rozwiązań, myśląc że to tylko kolejny kontener z nieco innym API. Mea culpa, przyznaję się, kiedyś sam się nadziałem, ale widzę że nie jestem sam ;).

28 stycznia 2014

Z LINQ jak po sznurku

Filed under: How Can I Take ... — Tagi: , , , , — Beniamin Zaborski @ 22:56

Każdy programista .NET słyszał, zna i zapewne używa LINQ. To bezsprzecznie świetny mechanizm. Świetny jest do tego stopnia, że teraz to środowisko Java przeszczepia go z .NET do siebie, a nie odwrotnie … ghhh. Sorry Javowcy taki teraz mamy klimat:).

Hola, hola … można by powiedzieć, bo w naszym fajnym LINQ można się rozbić o ścianę! Co gdy chce sobie dynamicznie złożyć zapytanie?

Pokażę to na podstawie trywialnego przykładu. Mamy listę obiektów:

var kontrahenci = new List<Kontrahent>() {
 new Kontrahent { Nazwa = "Kontrahent 1", Symbol="KWA1", CzyAktywny = true },
 new Kontrahent { Nazwa = "Kontrahent 2", Symbol="KSL2", CzyAktywny = false },
 new Kontrahent { Nazwa = "Kontrahent 3", Symbol="KWE2", CzyAktywny = false },
 new Kontrahent { Nazwa = "Kontrahent 4", Symbol="KNO2", CzyAktywny = true }
 };

i chcemy dynamicznie złożyć i wykonać poniższe zapytanie:

kontrahenci.Where(k => k.CzyAktywny && k.Symbol.Contains("2")).ToList();

Jak to zwykle bywa jest parę opcji, np.:

1. Użycie dynamic expressions:

 ParameterExpression kontrahentParam = Expression.Parameter(typeof(Kontrahent), "k");
 Expression leftCzyAktywnyExpression = Expression.Property(kontrahentParam, "CzyAktywny");
 Expression rightCzyAktywnyExpression = Expression.Constant(false);
 Expression czyAktywnyExpression = Expression.Equal(leftCzyAktywnyExpression, rightCzyAktywnyExpression);
 Expression leftSymbolExpression = Expression.Property(kontrahentParam, "Symbol");
 Expression rightSymbolExpression = Expression.Constant("2", typeof(string));
 Expression symbolExpression = Expression.Call(leftSymbolExpression, typeof(string).GetMethod("Contains", new[] { typeof(string) }), rightSymbolExpression);
 Expression andExpression = Expression.AndAlso(czyAktywnyExpression, symbolExpression);
 var whereExpression = Expression.Lambda<Func<Kontrahent, bool>>(andExpression, kontrahentParam).Compile();
 kontrahenci.AsQueryable().Where(whereExpression).ToList();

Wygląda okrutnie, a to przecież banalny przykład. Pomyślcie co się dzieje w tych naprawdę złożonych? Musi boleć.

2. Dynamic Expression API i zapytanie ze stringa:

kontrahenci.AsQueryable().Where("CzyAktywny == @0 and Symbol.Contains(@1)", false, "2").ToList();

Teraz lepiej? No myślę, że zdecydowanie!

A przed tym wszystkim wystarczy tylko:

1. Install-Package DynamicQuery

2. using System.Linq.Dynamic;

18 stycznia 2014

WCF – logowanie błędów

Może część z Was zastanawiała się kiedyś jak wprowadzić własny mechanizm logowania błędów w WCF? Ja miałem potrzebę implementacji takiego mechanizmu logowania i podzielę się jedną z prostszych opcji.

O co właściwie chodzi?

Mianowicie walczymy o to, aby po stronie serwera hostującego nasz serwis WCF była możliwość zapisu do logów błędów występujących w tymże serwisie. Wiemy, że domyślnie wszystkie wyjątki jakie wystąpią w serwisach WCF nie są wysyłane do klienta. Wiemy także, że to domyślne zachowanie możemy łatwo zmienić ustawiając includeExceptionDetailInFaults="true". Może na etapie developingu (tfu co za słowo) jest to wygodne, ale na pewno w systemie produkcyjnym … hmm no cóż powiedzmy, że klienta nie do końca interesują szczegóły wyjątku i cały stack trace:).  W systemie produkcyjnym chcemy mieć szczegóły błędów w naszym logu … czymkolwiek on będzie.

Z pomocą przychodzi nam prosty interfejs IErrorHandler z przestrzeni System.ServiceModel.Dispatcher. Zaimplementujmy go:

public class CustomErrorHandler : IErrorHandler
{
    public bool HandleError(Exception error)
    {
        Logger.LogError(error); // Tutaj użyj swojego loggera!
        return true;
    }

    public void ProvideFault(Exception error, MessageVersion version, ref Message msg)
    {

    }
}

To wszystko? Tak, a właściwie prawie:). Metoda HandleError to miejsce logowania błędu. Ale to jednak za mało gdyż jak się pewnie domyślacie teraz trzeba jakoś poinformować WCF-a, żeby użył naszej klasy CustomErrorHandler. Z pomocą przychodzi sam WCF, a mianowicie mechanizm rozszerzeń WCF-a, konkretniej mechanizm rozszerzeń zachowania serwisu BehaviorExtensionElement. A zatem:

public class CustomErrorHandlerExtension : BehaviorExtensionElement, IServiceBehavior
{
    public override Type BehaviorType
    {
        get { return GetType(); }
    }

    protected override object CreateBehavior()
    {
        return this;
    }

    private IErrorHandler GetInstance()
    {
        return new CustomErrorHandler();
    }

    void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
    {
    }

    void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {
        IErrorHandler errorHandlerInstance = GetInstance();
        foreach (ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers)
        {
            dispatcher.ErrorHandlers.Add(errorHandlerInstance);
        }
    }

    void IServiceBehavior.Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {

    }
}

Dwa najistotniejsze elementy powyższego kodu został wyróżnione, tj. instancjonowanie naszego CustomErrorHandler oraz zarejestrowanie go.

Czy teraz to już wszystko? Prawie :). Teraz przenosimy się do pliku konfiguracyjnego naszego serwera (w wypadu self-hosting to będzie pewnie usługa Windows, a w wypadku hostowania na IIS to jakaś aplikacja Web).

W ramach <system.serviceModel> dodajemy:

<extensions>
 <behaviorExtensions>
   <add name="customErrorHandler" type="TwojaAplikacja.CustomErrorHandlerExtension, TwojeAssembly" />
 </behaviorExtensions>
</extensions>

Rozszerzyliśmy WCF o nasz error handler, teraz pozostaje tylko go użyć. W ramach sekcji behavior naszego serwisu dodajemy <customErrorHandler/>.

Teraz to już naprawdę wszystko i działa!

5 Maj 2013

XPO jak Yeti

Filed under: How Can I Take ... — Tagi: , , — Beniamin Zaborski @ 18:50

XPO czyli eXpressPersistent Objects to komercyjny ORM firmy DevExpress. W naszym kraju to nieco egzotyczny produkt i jest z nim trochę jak z mitycznym Yeti – wszyscy słyszeli, widziało niewielu. Z czego wynika ta niezbyt duża popularność tego ORM-a? Czy winne jest temu to, że trzeba za niego płacić? Jakie ma wady, a jakie są jego zalety? Na te pytania jak i na kilka innych postaram się odpowiedzieć w tym wpisie.

Chciałbym zaznaczyć, że nie jestem ani nie byłem w jakikolwiek sposób związany z firmą DevExpress. Postaram się dość obiektywnie, ale jednak w jakimś stopniu przez pryzmat mojego doświadczenia, bliżej przyjrzeć XPO. Dzięki ukończeniu projektu wykorzystującego XPO jak i dzięki moim wcześniejszym doświadczeniom z innymi ORM-ami jak NHibernate czy Entity Framework, mogę spojrzeć na XPO z dużo szerzej perspektywy.

Swego czasu popełniłem wpis na blogu dotyczący Entity Framework. Chciałbym się odnieść właśnie do tego wpisu, co umożliwi w jakimś stopniu porównanie XPO z Entity Framework. Użyję dokładnie tego samego modelu klas, który wykorzystałem we wspomnianym wpisie. Oto on w postaci diagramu klas UML:

Obrazek

Model klas to ta część która z punktu biznesowego powinna nas interesować najbardziej. Sytuacja idealna to taka w której model nie ma świadomości o istnieniu ORM-a. Niestety jak się okazuje nie każdy ORM daje nam to o czym marzymy. Do tego tematu jeszcze wrócimy w dalszej części.

Z przedstawionego modelu klas możemy wywnioskować, że będziemy musieli uporać się z mapowaniem: typów prostych jak string czy bool, typów referencyjnych, relacji one-to-many czy many-to-many. Celowo pominąłem kwestię mapowania dziedziczenia, gdyż we wpisie do którego się odnoszę również nie było o tym mowy.

Zanim przystąpimy do mapowania klas na tabele bazy danych trzeba wspomnieć o dwóch głównych koncepcjach obsługi operacji CRUD jakie daje nam XPO. Pierwsza koncepcja zakłada, że klasy encji biznesowych będą dziedziczyć po specjalnych klasach bazowych XPO. W tym momencie włącza się nam czerwona lampka … grhhh. Z powodów oczywistych takie podejście odrzucamy już na wstępie. Pisałem o tym kiedyś w tym wpisie.
Na szczęście (to się dopiero okaże) dokumentacja wspomina także o odmiennym podejściu zwanym session-less. Dzięki temu nasze encje biznesowe pozostaną POCO. To cieszy i jest na plus dla XPO. Oznaczać to będzie także, że wykonywanie operacji CRUD na obiektach będzie się odbywało za pomocą obiektu sesji, a nie jak w pierwszej koncepcji za pomocą dedykowanych do tego celu metod wewnątrz obiektów. No wszystko logiczne – przecież dokładnie tego oczekiwaliśmy.

Pora najwyższa zabrać się wreszcie za mapowanie klas na tabele bazy danych. Jedynym oficjalnym sposobem mapowania są atrybuty. Oczywiście zaletą takiego rozwiązania jest wygoda. Bezwzględnie wadą w takim podejściu jest „zabrudzenie” modelu „obcymi” atrybutami. Trochę żal że producent nie zdecydował się na dostarczenie alternatywnej i „czystszej” metody mapowania. Konkurencja w postaci twórców NHibernate czy Entity Framework przemyślała to zdecydowanie bardziej.
Kolejna wada XPO, chociaż nie jest to typowa przypadłość tego ORM-a, to potrzeba odwzorowywania kolekcji poprzez specjalny typ dostarczany przez XPO czyli XPCollection. W tej kwestii Entity Framework wymiata! Mając to na uwadze już wiemy, że będziemy musieli zmodyfikować nasz model klas zastępując typy wszystkich kolekcji IList typem XPCollection.

Aby poinformować XPO o tym iż dana klasa ma być mapowana na tabelę bazy danych należy oznaczyć ją atrybutem Persistent z opcjonalnym parametrem określającym nazwę tabeli w bazie danych. Dokładnie ten sam atrybut można zaaplikować dowolnej właściwości podając opcjonalnie nazwę kolumny tabeli. Mapowanie właściwości typów prostych jak i referencyjnych jest banalne. A co z kolekcjami? Tu zaczynają się schody. Oczywiście pomijając to, że wszystkie nasze kolekcje muszą być typu XPCollection (to już zaakceptowaliśmy – czyżby?), to musimy te kolekcje jeszcze zainicjować. Na nieszczęście potrzebny do tego będzie nam obiekt sesji. Hmmm … zatem stwierdzenie session-less wydaje się być trochę na wyrost. Niestety tak jest i to boli!

Poniżej przedstawiam kompletny model klas dostosowany do wymogów XPO wraz z pełnym mapowaniem:

    [Persistent(„Komputery”)]
    public class Komputer : BusinessEntity
    {
        public Komputer() : base() { }
        public Komputer(Session session) : base(session) { }
        [Persistent(„Nazwa”), Size(120)]
        public string Nazwa { get; set; }
        [Persistent(„AdresIP”), Size(14)]
        public string AdresIP { get; set; }
        [Association(„Uzytkownik-Komputery”)]
        public Uzytkownik Uzytkownik { get; set; }
    }
    [Persistent(„Role”)]
    public class Rola : BusinessEntity
    {
        public Rola() : base() { }
        public Rola(Session session) : base(session)
        {
            this.uprawnienia = new XPCollection<Uprawnienie>(session, this, session.GetClassInfo<Rola>().GetMember(„Uprawnienia”));
        }
        [Persistent(„Nazwa”), Size(120)]
        public string Nazwa { get; set; }
        private XPCollection<Uprawnienie> uprawnienia;
        [Association(„Role-Uprawnienia”)]
        public XPCollection<Uprawnienie> Uprawnienia
        {
            get { return uprawnienia; }
        }
    }
    [Persistent(„Uprawnienia”)]
    public class Uprawnienie : BusinessEntity
    {
        public Uprawnienie() : base() { }
        public Uprawnienie(Session session) : base(session)
        {
            this.role = new XPCollection<Rola>(session, this, session.GetClassInfo<Uprawnienie>().GetMember(„Role”));
        }
        [Persistent(„Nazwa”), Size(120)]
        public string Nazwa { get; set; }
        private XPCollection<Rola> role;
        [Association(„Role-Uprawnienia”)]
        public XPCollection<Rola> Role
        {
            get { return role; }
        }
    }
    [Persistent(„Uzytkownicy”)]
    public class Uzytkownik : BusinessEntity
    {
        public Uzytkownik() : base() { }
        public Uzytkownik(Session session) : base(session)
        {
            this.komputery = new XPCollection<Komputer>(session, this, session.GetClassInfo<Uzytkownik>().GetMember(„Komputery”));
        }
        [Persistent(„Login”), Size(120)]
        public string Login { get; set; }
        [Persistent(„CzyAktywny”)]
        public bool CzyAktywny { get; set; }
        [Persistent(„Rola”)]
        public Rola Rola { get; set; }
        private XPCollection<Komputer> komputery;
        [Association(„Uzytkownik-Komputery”), Aggregated]
        [Delayed]
        public XPCollection<Komputer> Komputery
        {
            get { return komputery; }
        }
    }

Na powyższym listingu można dostrzec coś o czym jak dotąd nie wspomniałem – atrybut Association. Jest to atrybut który musi być zaaplikowany na obu końcach relacji one-to-many oraz many-to-many. Mało tego, gdy chcemy dodać kolekcję elementów pewnego typu do jednej z klas zostaniemy zmuszeni do dodania do klasy będącej elementem tej kolekcji wskazania na obiekt zawierający tą kolekcję. Przykładem może być właściwość Uzytkownik w klasie Komputer, której tak naprawdę nie chciałem tam mieć. Nie wygląda to zbyt dobrze, gdyż płacimy często dość wysoką cenę za dostosowywanie modelu do wymagów XPO. Mnie osobiście nie łatwo się z tym pogodzić i zaliczam to do jednych z najpoważniejszych ograniczeń XPO.

Wspomnieć trzeba jeszcze o atrybucie Aggregated, który jest w pewnym stopniu odpowiednikiem właściwości cascade z NHibernate. Nałożenie tego atrybut na właściwość typu referencyjnego lub kolekcję oznaczać będzie, że obiekty te będą zapisywane wraz z zapisem obiektu głównego. W takim wypadku nie będzie wymagany zapis np. poszczególnych elementów kolekcji.

XPO obsługuje także mechanizm lazy-loading, czyli ładowanie właściwości na żądanie przy pierwszym dostępnie do nich. Zachowanie XPO w tym względzie jest jednak zupełnie odmienne od tego które znamy z Entity Framework. W XPO domyślnie lazy-loding jest wyłączony i aby wskazać, że dana właściwość ma być doładowywana na żądanie należy oznaczyć ją atrybutem Delayed.

Bardzo bogato wygląda natomiast pobieranie danych w XPO. Mamy tu do dyspozycji tzw. operatory kryteriów, LINQ czy nawet zapytania SQL. Mnie najbardziej chyba ucieszył mechanizm Linq-to-XPO, chociaż muszę przyznać, że operatory kryteriów – po opanowaniu ich – także dają spore możliwości.

Aby rozpocząć manipulowanie obiektami trzeba zainicjować wspominaną już tutaj niejednokrotnie sesję. Sposobów na to jest kilka, ale jednym z prostszych jest utworzenie DataLayer, a następnie przekazanie go do konstruktora sesji, jak poniżej:

IDataLayer dataLayer = XpoDefault.GetDataLayer(connectionString, AutoCreateOption.DatabaseAndSchema);
Session session = new Session(dataLayer);

Nie napisałem dotąd nic o tworzeniu samej bazy danych oraz tworzeniu obiektów wewnątrz niej jak tabele, klucze główne, klucze obce, itp. Podczas tworzenia obiektu dataLayer, XPO pozwala nam określić jego zachowanie w kwestii automatycznego tworzenia obiektów w bazie danych.

Zachowanie XPO wymuszamy z pomocą typu wyliczeniowego AutoCreateOption:

DatabaseAndSchema – tworzy bazę danych oraz pełny schemat bazy nawet z kluczami obcymi

None – nie robi nic, tzn. nie tworzy automatycznie bazy danych, ani żadnych obiektów w niej

SchemaOnly – tworzy tylko schemat w bazie danych, ale tylko jeśli baza istnieje

SchemaAlreadyExists – nie weryfikuje schematu bazy pod kątem zgodności z aktualnym stanem obiektów

W sytuacji gdy całą pracę związaną z tworzeniem schematu bazy danych przerzucimy na XPO to warto zaznaczyć, że XPO całkiem sprawnie radzi sobie z modyfikacjami schematu na podstawie wprowadzonych zmian w modelu klas.

Pełny projekt prostej aplikacji konsolowej na której bazuje ten opis wykonanej w C# dla VS 2012 można pobrać stąd.

Podsumowując moje odczucia co do XPO to na pewno do zalet zaliczył bym prostotę tworzenia mapowania poprzez atrybuty, mnogość sposobów pobierania danych oraz dobry mechanizm tworzenia i aktualizacji schematu bazy danych. Główne wady to obsługa kolekcji i podejście session-less (trochę jakby oszukane).

Względnie nieduża popularność XPO jako ORM-a wynikać może właśnie z tych kilku ale jednak znaczących wad. Poza tym odnoszę wrażenie, że producent traktuje XPO jako dodatek do całej reszty swoich produktów, a nie główny produkt który chce sprzedawać.

Po bliższym zapoznaniu się z XPO sami musicie zdecydować czy jest to ORM na którym możecie oprzeć Wasz następny projekt. Napewno jest warty zainteresowania, ale być może sprawdzi się tylko w pewnej specyficznej grupie projektów.

30 Maj 2011

Entity Framework code-first – pierwsze koty za płoty

W ostatnim czasie pojawiło się kilka zmian w Entity Framework obok których nie sposób przejść obojętnie. Od dawna przyglądam się rozwojowi tego O/R mappera, ale jak dotąd nigdy nie byłem jeszcze tak podekscytowany jak teraz. Co takiego wywołuje ową ekscytację? Code-first! Tak to jest to. Z pełną świadomością mogę powiedzieć, że dla mnie code-first = Entity Framework. Wszystko co jest poza tym w EF już może nie istnieć. Ale postarajmy się to nieco uporządkować.

Entity Framework dostarcza trzy różne podejścia do odwzorowywania encji modelu domeny na relacyjne obiekty bazy danych:

– database-first: zaczynamy od utworzenia bazy danych, a EF automatycznie generuje klasy modelu na podstawie struktury bazy danych

– model-first: zaczynamy od utworzenia klas modelu w dedykowanym designerze i mapujemy je na istniejącą bazę danych lub EF generuje bazę na podstawie stworzonego modelu

– code-first: zaczynamy od utworzenia klas modelu wprost w kodzie i mapujemy na obiekty bazy danych.

W dalszej części zamierzam skupić się na tym ostatnim podejściu. Według mojego może odrobinę subiektywnego odczucia jest to jedyny wartościowy sposób użycia EF. Kontrowersyjne? Otóż nie do końca. Patrząc przez pryzmat programisty używającego przez kilka lat NHibernate (ostrzegałem, że może nie być obiektywnie) takie podejście wydaje się być zupełnie naturalne.

Rozpoczynam od utworzenia obiektów modelu domeny – obiektów POCO rzecz jasna – nie myśląc na tym etapie o projektowaniu relacji bazy danych (Domain Driven Development). W następnej kolejności tworzę odpowiednią strukturę bazy danych i mapuję na nią klasy modelu. Entity Framework potrafi sam wygenerować strukturę bazy danych na podstawie klas modelu stosując analogiczne nazewnictwo do nazw klas i ich właściwości. Wygodne rozwiązanie, jednak życie pokazuje, że mało praktyczne szczególnie w dużych projektach. Każdy z nas ma jakieś przyzwyczajenia co do nazewnictwa obiektów w bazie danych – ja również i one się nie pokrywają z konwencjami nazewniczymi dostarczanymi przez EF. Przyzwyczajenia owszem można zmienić, co jednak gdy korporacja posiada ścisłe reguły co do nazywania obiektów w bazie danych? Głową muru się nie przebije, stąd potrzeba elastycznego mechanizmu konfiguracji mapowania. Na początku się obawiałem na jak wiele pozwoli nam tutaj Microsoft. Niepotrzebnie, bo za pomocą fluent API można naprawdę dużo. Nie jest to co prawda tak dużo jak oferuje NHibernate, ale nie powinno to wpłynąć na wygodę używania Entity Framework Code-First.

Korzystając z podejścia code-first mamy trzy sposoby na mapowanie obiektowo-relacyjne. Po pierwsze, wspomniane już, automatyczne generowanie struktury bazy danych; po drugie mapowanie przy pomocy atrybutów z przestrzeni nazw System.Data.Annotations; i po trzecie preferowane przeze mnie ze względu na swoje możliwości – fluent API.

Nie uważam, aby mapowanie przy pomocy atrybutów było złym rozwiązaniem, po prostu fluent API wydaje mi się lepsze. Dzięki temu mogę odseparować model od bazy danych i stworzyć go bardziej „czystym”. Pod tym względem zdecydowanie jestem purystą.

Przeanalizujmy sposób użycia fluent API na podstawie projektu prostej aplikacji konsolowej stworzonej w Visual Studio, którą można pobrać stąd. Trywialny model domeny (mocno anemiczny) posiada jedynie cztery klasy. Nie w tym rzecz jednak. Różne relacje między tymi klasami obrazują to co najważniejsze w kontekście konfiguracji mappingu. Projekt pokazuje jak zmapować relacje między obiektami typu: many-to-one (Uzytkownik->Rola), one-to-many (Uzytkownik->Komputer) czy many-to-many (Rola->Uprawnienie).

Przejdźmy do tworzenia wspomnianego modelu domeny, dodając do projektu cztery następujące klasy:

public class Komputer

{

public long? Id { get; set; }

public string Nazwa { get; set; }

public string AdresIP { get; set; }

}

public class Uprawnienie

{

public long? Id { get; set; }

public string Nazwa { get; set; }

public IList<Rola> Role { get; set; }

}

public class Rola

{

public long? Id { get; set; }

public string Nazwa { get; set; }

public IList<Uprawnienie> Uprawnienia { get; set; }

}

public class Uzytkownik

{

public long? Id { get; set; }

public string Login { get; set; }

public bool CzyAktywny { get; set; }

public Rola Rola { get; set; }

public IList<Komputer> Komputery { get; set; }

}

Kolejnym krokiem jest wygenerowanie stosownej struktury tabel w bazie danych zgodnie z przyjętą przez siebie/organizację konwencją. Pomijam opis tego procesu, bo może być wykonany na wiele sposobów. Począwszy od „ręcznego” utworzenia tabel, aż po skorzystanie z własnych dedykowanych do tego narzędzi. Niejednokrotnie jest tak, że otrzymujemy już gotową strukturę bazy danych od programisty baz danych. W załączonym projekcie znajduje się skrypt SQL tworzący wymaganą strukturę bazy danych (dla MS SQL Server).

Mamy model domeny w postaci klas, mamy strukturę bazy danych – czas przejść do mapowania. Zanim jednak to zrobimy, dodajmy jeszcze connection string do naszej bazy danych do pliku konfiguracyjnego aplikacji app.config:

<connectionStrings>

<clear/>

<addname=ConnStr

connectionString=Data Source=(local);Initial Catalog=EF_CODE_FIRST_SAMPLE_DB;Integrated Security=SSPI;

providerName=System.Data.SqlClient />

</connectionStrings>

Kluczowym elementem aplikacji z wykorzystaniem Entity Framework code-first jest klasa DbContext z przestrzeni nazw System.Data.Entity. Pełni ona rolę wzorca Repository, a także jednocześnie Unit Of Work. To dzięki tej klasie możemy wykonywać na encjach modelu wszelkie operacie CRUD w ramach transakcji. Oznacza to, że naszym kolejnym krokiem jest utworzenie właśnie klasy dziedziczącej po DbContext. Zróbmy to:

public class ModelContext : DbContext

{

public ModelContext() : base() { }

public ModelContext(string stringNameOrConnectionString) : base(stringNameOrConnectionString) { }

}

Użyłem tu przeciążenia konstruktora, które pozwoli nam podać nazwę connection stringa z pliku konfiguracyjnego lub wprost podać connection string do bazy danych.

Kolejna bardzo istotna klasa, funkcjonująca nierozerwalnie z DbContext, to DbSet<>. Reprezentuje kolekcję encji danego typu w ramach kontekstu jaki definiuje nam klasa DbContext. Dodajmy zatem stosowne kolekcje encji do klasy ModelContext:

public DbSet<Rola> Role { get; set; }

public DbSet<Uzytkownik> Uzytkownicy { get; set; }

W tym momencie możemy przejść już do samego mapowania z użyciem fluent API. W związku z tym w klasie ModelContext musimy przesłonić metodę OnModelCreating:

protected override void OnModelCreating(DbModelBuilder modelBuilder)

{

base.OnModelCreating(modelBuilder);

}

To jest właśnie to miejsce w którym definiujemy konfigurację mappingu. Możemy to zrobić wprost w tej metodzie lub lepszym pomysłem będzie wydzielenie mappingu poszczególnych encji do dedykowanych klas. Sprawdza się to szczególnie w większych projektach. W tym celu należy utworzyć klasę dziedziczącą po EntityTypeConfiguration<> dla każdej encji:

public class KomputerMapping : EntityTypeConfiguration<Komputer>

{

public KomputerMapping()

: base()

{

this.HasKey(e => e.Id).Property(e => e.Id).HasColumnName(„ID_KOMPUTERA”);

this.Property(e => e.Nazwa).HasColumnName(„NAZWA”);

this.Property(e => e.AdresIP).HasColumnName(„ADRES_IP”);

this.ToTable(„KOMPUTERY”);

}

}

public class UprawnienieMapping : EntityTypeConfiguration<Uprawnienie>

{

public UprawnienieMapping()

: base()

{

this.HasKey(e => e.Id).Property(e => e.Id).HasColumnName(„ID_UPRAWNIENIA”);

this.Property(e => e.Nazwa).HasColumnName(„NAZWA”);

this.ToTable(„UPRAWNIENIA”);

}

}

public class RolaMapping : EntityTypeConfiguration<Rola>

{

public RolaMapping()

: base()

{

this.HasKey(e => e.Id).Property(e => e.Id).HasColumnName(„ID_ROLI”);

this.Property(e => e.Nazwa).HasColumnName(„NAZWA”);

this.HasMany<Uprawnienie>(u => u.Uprawnienia).WithMany(u => u.Role).Map(

m => m.ToTable(„UPRAWNIENIA_DLA_ROLI”)

.MapLeftKey(„ID_ROLI”)

.MapRightKey(„ID_UPRAWNIENIA”)

);

this.ToTable(„ROLE”);

}

}

public class UzytkownikMapping : EntityTypeConfiguration<Uzytkownik>

{

public UzytkownikMapping()

: base()

{

this.HasKey(e => e.Id).Property(e => e.Id).HasColumnName(„ID_UZYTKOWNIKA”);

this.Property(e => e.Login).HasColumnName(„LOGIN”);

this.Property(e => e.CzyAktywny).HasColumnName(„CZY_AKTYWNY”);

this.HasRequired(e => e.Rola).WithMany().Map(p=>p.MapKey(„ID_ROLI”));

this.HasMany(e => e.Komputery).WithOptional().Map(p => p.MapKey(„ID_UZYTKOWNIKA”)).WillCascadeOnDelete();

this.ToTable(„UZYTKOWNICY”);

}

}

To jest kompletny mapping dla naszego modelu domeny. Przyjrzyjmy się kluczowym elementom. Wywołanie HasKey() wskazuje które property jest primary key w naszej encji, natomiast Property() umożliwia zmapowanie konkretnej właściwości na konkretną kolumnę w tabeli bazy danych. Użyłem wywołania HasColumnName(), aby wskazać inną nazwę kolumny w tabeli – nieodpowiadającą konwencji nazewniczej EF. To wszystko okraszone jest wywołaniem ToTable(), gdzie wskazujemy nazwę tabeli odpowiadającą encji. Mapowanie klucza głównego i właściwości typów prostych wygląda na niezbyt skomplikowane. Czas przejść do mapowania relacji między obiektami.

Na pierwszy ogień many-to-one czyli zwykła referencja do innego typu encji z naszego modelu. Dobrym przykładem jest powiązanie użytkownika z rolą. Klasa Uzytkownik posiada referencję do klasy Rola, przy czym jest to referencja wymagana. Mapujemy to za pomocą wywołania HasRequired() wskazując nazwę właściwości przechowującej referencję do klasy Rola. W związku z tym, że także w tym wypadku konwencja nazewnicza klucza obcego w tabeli nie odpowiada tej z EF należy wskazać nazwę. Robimy to przy pomocy wywołania MapKey() podając nazwę klucza obcego w tabeli. Kompletny przykład mapowania takiej relacji wygląda następująco:

this.HasRequired(e => e.Rola).WithMany().Map(p=>p.MapKey(„ID_ROLI”));

W sytuacji kiedy property przechowujące referencję nie jest wymagane, to zamiast HasRequired() użyjemy HasOptional().

Innym typem relacji jest kolekcja czyli one-to-many. Przeanalizujmy to na przykładzie kolekcji elementów Komputer w klasie Uzytkownik. Do zmapowania listy używamy wywołania HasMany() podając jako parametr property – za pomocą wyrażenia lambda oczywiście – które wskazuje naszą kolekcję. W związku z tym, iż ta właściwość nie jest wymagana używamy WithOptional(). Jak się wszyscy domyślają w przeciwnym wypadku użylibyśmy WithRequired(). Wywołanie Map() z MapKey() wygląda już znajomo i używamy go by wskazać klucz obcy w tabeli zawierającej rekordy będące elementami listy. Dodatkowo na końcu znajduje się wywołanie metody WillCascadeOnDelete(). Cóż to takiego? Nic innego jak wskazanie czy podczas usunięcia elementu nadrzędnego ma być wywołane kaskadowe usunięcie elementów podrzędnych, czyli tych w naszej kolekcji. Cały przykład wygląda następująco:

this.HasMany(e => e.Komputery).WithOptional().Map(p => p.MapKey(„ID_UZYTKOWNIKA”)).WillCascadeOnDelete();

Na koniec najbardziej skomplikowana relacja czyli many-to-many. Przykładem takiego powiązania jest wzajemna relacja między obiektami Rola i Uprawnienie. Rola posiada kolekcję elementów Uprawnienie i vice versa, tj. Uprawnienie posiada kolekcję elementów Rola. Aby zmapować takie powiązanie na postać relacyjną potrzebujemy dodatkowej tabeli. Ta tabela przechowuje pary identyfikatorów roli i uprawnienia. Dobrze, aby kluczem głównym takiej tabeli była owa para identyfikatorów. Mapowania wykonujemy z jednej ze stron np. z encji Rola. Spójrzmy na przykład:

this.HasMany<Uprawnienie>(u => u.Uprawnienia).WithMany(u => u.Role).Map(

m => m.ToTable(„UPRAWNIENIA_DLA_ROLI”)

.MapLeftKey(„ID_ROLI”)

.MapRightKey(„ID_UPRAWNIENIA”)

);

Wywołujemy generyczną wersję metody HasMany, podając jako parametr generyczny typ elementu kolekcji, a jako parametr wywołania właściwość klasy przechowującą kolekcję tych elementów. Następnie w wywołaniu WithMany podajemy wskazanie w drugą stronę tj. w tym przypadku na kolekcję elementów Rola. Ostatni element mapowania relacji wiele do wielu i najważniejszy to wskazanie wspomnianej tabeli powiązania w Map(). Nazwę tabeli podajemy w wywołaniu ToTable i wskazujemy nazwy kolumn kluczy obcych do obu typów encji/tabel. Należy dodać, że left key to klucz obcy wskazujący na typ którego mapping opisujemy, a right key to klucz obcy wskazujący na typ elementów kolekcji.

Tak by wyglądał przyznaje, że dość pobieżny opis mapowania encji w moim prostym projekcie. Uważam, że fluent API jest na tyle intuicyjne i samo-opisujące iż czysty kod wielu z Was najlepiej pomoże uchwycić to wszystko.

Ostatnim krokiem jest wskazanie, które z klas „konfiguracyjnych” definiują mapping których encji. To bardzo proste – w metodzie OnModelCreating dodajemy:

modelBuilder.Configurations.Add(newRolaMapping());

modelBuilder.Configurations.Add(newUzytkownikMapping());

modelBuilder.Configurations.Add(newKomputerMapping());

modelBuilder.Configurations.Add(newUprawnienieMapping());

Zachęcam jeszcze do przeanalizowania kompletnego projektu zamieszczonego tutaj.

Na koniec nie mógłbym nie podkreślić tego, iż trzymam kciuki za dalszy rozwój Entity Framework właśnie w tym kierunku – jedynym słusznym kierunku.

30 listopada 2009

Error Provider

Filed under: How Can I Take ... — Tagi: , , , — Beniamin Zaborski @ 19:51

W poniższym artykule chciałbym poruszyć kwestię związana z walidacją obiektów. Oczywiste jest, że walidacja jest niezbędnym elementem każdej dobrze zaprojektowanej aplikacji biznesowej. Jednak nie sam sposób walidowania obiektów jest przewodnim tematem tego artykułu. Chciałbym przedstawić pewien dość oczywisty sposób informowania użytkownika o błędach. Do napisania tego artykułu zainspirowało mnie pytanie mojego znajomego … właśnie na podobny temat. Okazuje się, że nie wszystko jest tak oczywiste jak sądzimy. Na wstępie chciałbym zaznaczyć, iż artykuł dotyczy aplikacji Window Forms.
Większość z nas pewnie widziała w aplikacjach ASP.NET ikonki przy polach edycyjnych informujące o wymagalności danego pola lub zbyt dużej ilość znaków – ogólnie o niespełnionych regułach walidacyjnych. Podobny efekt możemy uzyskać w aplikacjach Window Forms. Tu kolejna dobra wiadomość – za pomocą standardowych kontrolek i to w łatwy sposób. Odpowiedzialna za to jest kontrolka o nazwie ErrorProvider. Poniższy zrzut ekranu przedstawia wspomnianą kontrolkę w akcji.

Niespełniona reguła walidacyjna na wskazanym property obiektu powoduje wyświetlenie ikony przy odpowiedniej kontrolce. Szczegóły niespełnionych reguł są wyświetlane w tooltipie. Wiemy zatem co chcemy uzyskać, teraz pytanie jak?

Zacznę może od samego obiektu który podlega walidacji. W aplikacjach biznesowych jest to z reguły jakiś DataTransferObject. Kontrolka ErrorProvider potrzebuje, aby dostarczyć do niej informacji o niespełnionych regułach walidacyjnych. Ta informacja to zwykły string czyli komunikat który ma się wyświetlić użytkownikowi. Odpowiedzialny za to jest interfejs IDataErrorInfo.

Interfejs jest dość prosty bo posiada tylko dwie właściwości:

  • string Error { get; }
  • string Item[string columnName] { get; }

Jak się okazuje dalej implementacja tego interfejsu w naszej klasie DataTransferObject również nie jest trudna. Zakładam, że nasz DataTransferObject posiada już sam mechanizm walidowania np. wykorzystujący całkiem poręczny Application Validation Block pochodzący z Enterprise Library. Nie pozostaje teraz nic innego jak zaimplementować IDataErrorInfo w naszej klasie DataTransferObject.

public abstract class DataTransferObject : IDataTransferObject, IValidatable, IDataErrorInfo
{
// pominięta część kodu klasy nieistotna w omawianym kontekście
string IDataErrorInfo.Error
{
get
{
if (!IsValid)
return BrokenValidationRules.GetDescriptionBrokenRules();
else
return String.Empty;
}
}

string IDataErrorInfo.this[string columnName]
{
get
{
string result = string.Empty;
if (!IsValid)
{
IBrokenValidationRule brokenRule = BrokenValidationRules.GetBrokenRule(columnName);
if (brokenRule != null)
result = brokenRule.Description;
}
return result;
}
}
}

Właściwość Error zwraca nam listę komunikatów o niespełnionych regułach na wskazanym obiekcie. Indekser zwraca nam natomiast listę komunikatów niespełnionych reguł dla wskazanej właściwości obiektu. Parametr columnName określa nazwę właściwości obiektu.

Przedstawiona klasa nie posiada pełnej implementacji, a jedynie te elementy które są istotne w omawianym kontekście. Oczywiste jest, że klasa musi mieć w tym wypadku kolekcję niespełnionych reguł walidacyjnych, która jest odpowiednio uzupełniana podczas wywołania walidacji. Na tej kolekcji zresztą bazują zaimplementowane właściwości z interfejsu IDataErrorInfo. W ten sposób chciałem pokazać, iż sam mechanizm walidacji jest zupełnie niezależny od implementacji IDataErrorInfo i po wprowadzeniu kilku własnych interfejsów może być łatwo wymieniany na inny.

Teraz możemy zająć się już interfejsem użytkownika. Pierwsze co należy zrobić to położyć na widoku kontrolkę ErrorProvider, np. przeciągając z toolboxa. Kolejnym krokiem jest zbindowanie naszego obiektu (dziedziczącego z DataTransferObject) z kontrolkami i przypisanie tego obiektu jako źródła danych do właściwości DataSource kontrolki ErrorProvider. Jeśli dodajemy kontrolkę ErrorProvider nie poprzez designera warto pamiętać o ustawieniu właściwości ContainerControl na odpowiednią formę/kontrolkę.

Sama kontrolka ErrorProvider posiada jeszcze kilka ciekawych właściwości jak: BlinkRate (częstotliwość pulsowania), BlinkStyle (kiedy ma pulsować ikonka) oraz Icon.

Powodzenia w implementacji wizualizacji wyników walidacji we własnej aplikacji w ten jakże prosty sposób.

16 listopada 2009

O2O Mapping

Filed under: How Can I Take ... — Tagi: , , , — Beniamin Zaborski @ 23:46

Zainspirowany jednym z postów na blogu Macieja Aniserowicza, postanowiłem bliżej przyjrzeć się zagadnieniu mapowania object-to-object. Wydaje się to być idealny lek na „głupie” mapowanie property poprzez przepisywanie każdego po kolei. A i owszem jest. Szczególnie przydatne przy mapowaniu obiektów domeny do obiektów DTO. Nie wiem ilu z Was robi/robiło to właśnie w ten tzw. „głupi” sposób, ale ja muszę przyznać że … mhm. Przykre, jak czasem nie zauważamy tak prostych rozwiązań pozwalających zaoszczędzić sporo czasu i zbędnych linii kodu. AutoMapper bo to o nim właściwie pisał Maciej to porządne narzędzie, które powinno zainteresować każdego programistę .NET. Na pierwszy rzut oka wygląda zachwycająco, ale to chyba bardziej idea tego typu mapowania jest bardziej zachwycająca niż sam w sobie produkt. Narzekam? Nie w sumie to nie. AutoMapper jest ok, ale niektóre rozwiązania w nim mnie nie odpowiadały. Sprawdziłem inne narzędzia tego typu jak choćby Glue i … ja potrzebowałem czegoś naprawdę trywialnego, bo sama idea jest trywialna. Długo nie myśląc stwierdziłem, że te kilka klas to napisze sam w taki sposób, aby mnie odpowiadały w 100%. I stało się – od pomysłu, po implementację. Czego potrzebowałem? Niczego więcej jak mapowania ad-hoc z możliwością zdefiniowania własnych reguł ustawiania wartości właściwości w obiekcie docelowym.
Oto przykład mówiący wszystko za siebie:


User user = new User();
user.Id = Guid.NewGuid();
user.Login = „Jan”;
user.Password = „Secret”;
user.FirstName = „Jan”;
user.LastName = „Kowalski”;
user.RoleName = „Admins”;

var mapper = new ObjectMapper<User, UserDTO>();
mapper
  .AddRule(„FullName”, s => s.FirstName + ” ” + s.LastName)
  .AddRule(„Login”, s => s.Login)
  .AddRule(„Password”, s=>SecurityUtils.Md5(s.Password));
mapper.Ignore(„RoleName”);
UserDTO userDTO = mapper.Map(user);

Kod źródłowy do wglądu zamieszczam tutaj. Tak na marginesie cały pomysł jak i jego implementacja powstała podczas jednego z programów rozrywkowych z tępym żartem granego w pewnej stacji na trzy litery w poniedziałkowe wieczory. A więc jak sądzę wszystko to trwało nie więcej jak 60 minut (z reklamami) – tłumaczę się na zaś jakby jednak coś nie działało;).

13 października 2009

Spring.NET vs WCF

Filed under: How Can I Take ... — Tagi: , , , — Beniamin Zaborski @ 23:04

Tytuł artykułu zdradza nieco temat jaki chciałbym podjąć. Może samo versus jest nieco przewrotne, gdyż bardziej odpowiednie byłoby „Spring.NET a WCF” czy też po prostu „Spring.NET i WCF”. Tak naprawdę to tytuł powinien brzmieć „Spring.NET a WCF versus programista” ;). Obserwując programistów zaczynających przygodę ze Spring.NET i próbujących zintegrować z tym frameworkiem technologię WCF właśnie taki tytuł się nasuwa. Wielu programistów napotyka na problemy przy integracji tych rozwiązań. Zatem, moim zadaniem będzie zaprezentowanie prostej aplikacji pozwalającej hostować serwisy po technologii WCF w oparciu o Spring.NET.
W jednym z moich artykułów z serii „Ugryźć Spring.NET” poświęconych serwisom (cz.6) napisałem, że zmiana technologii hostowania serwisów wymagać będzie jedynie zmian w konfiguracji. W dużej mierze jest to oczywiście prawda. Jednak są pewne niuanse, które wymagają wyjaśnienia. W przykładzie w tamtym artykule serwisy były hostowane za pomocą .NET Remotingu, tu będzie to WCF.
A teraz uwaga skierowana w stronę wszystkich tutoriali do WCF jakie widziałem. Uważam, że wszystkie te tutoriale uczą skrajnie niepraktycznego podejścia do jednej z kwestii – client proxy. Chodzi tu o generowanie klas proxy klienta za pomocą bądź to bezpośrednio środowiska Visual Studio, bądź narzędzia svcutil.exe. Jak dla mnie wiąże się z tym podstawowa niedogodność, iż w trakcie tworzenia aplikacji mój serwis musi być już hostowany, abym mógł skorzystać z dobrodziejstw narzędzia svcutil.exe. Poza tym każda zmiana w serwisie wymaga ponownego przegenerowania. Koszmar!!! Wady takiego rozwiązania można by jeszcze mnożyć – serwisy są „mało abstrakcyjne” czyli zbyt powiązane z implementacją opartą na WCF.
Proste podejście do którego się już przyzwyczaiłem to osobne assembly z interfejsem modelu aplikacji i osobne assembly z implementacją tego modelu (czytaj serwisów).
Dlaczego taki model, uważany nie tylko przeze mnie za właściwy, ma być zburzony przez WCF? Ależ wcale nie musi! Gdyby tylko w tych tutorialach więcej pisano o klasie ChannelFactory i metodzie CreateChannel …
Z tego rozwiązania korzysta właśnie Spring.NET. Dzięki temu migrując serwisy naszej aplikacji z .NET Remotnigu do WCF nie burzymy naszej aplikacji, a jedynie dokonujemy zmian w konfiguracji. Pozostaje tu oczywiście jedna kwestia do wyjaśnienia, czyli atrybutów WCF-a, tj. ServiceContract oraz OperationContract. One niestety muszą być użyte, więc to będzie cała zmiana w kodzie aplikacji – reszta to config. Nasze obiekty DTO są zapewne już opatrzone atrybutem Serializable i wcale nie musimy tego zmieniać na DataContract.
Wyjaśniłem chyba już trochę, a zatem czas na przykład. Przykład jest trywialny.
Solution składa się z czterech projektów:

  • interfejs modelu (BeezDev.SpringWCFApplication.Model.Interface)
  • implementacja modelu (BeezDev.SpringWCFApplication.Model)
  • konsolowa aplikacja hostująca serwisy aka server (BeezDev.SpringWCFApplication.Server)
  • okienkowa aplikacja klient (BeezDev.SpringWCFApplication.Client)

Aplikacja posiada jeden serwis HelloService z jedną metodą SayHello przyjmującą jako parametr jeden obiekt DTO i zwracającą inny typ obiektu DTO. Jakże wyszukana logika mówi: „podaj mi swój login, a zostaniesz przywitany”. Aplikacja serwera posiada referencję do interfejsu modelu jak i jego implementacji, natomiast już klient posiada tylko referencję do interfejsu modelu.
Przyjrzyjmy się najpierw konfiguracji serwera. Nie będę oczywiście omawiał całości konfiguracji Spring.NET, a jedynie te elementy które są istotne w kontekście WCF. Pierwsza sprawa to definicja serwisu w kontenerze IoC Springa.
Generalnie przy migracji z .NET Remoting ona istnieje. Jedyna zmiana, której ewentualnie musimy dokonać, to oznaczyć serwis aby nie był uruchamiany jako singleton (singleton=”false”). Jak podaje dokumentacja Spring.NET ten typ aktywacji obiektu nie współpracuje poprawnie z technologią WCF. Przyjmijmy to jako pewnik i na tym etapie nie wnikajmy w szczegóły.
Cała moc Springa drzemie w eksporterach serwisów, a więc należy zdefiniować odpowiedni dla WCF:


<object id=”helloServiceHost” type=”Spring.ServiceModel.ServiceExporter, Spring.Services”>
  <property name=”TargetName” value=”helloService” />
</object>

Nie ma tu nic nowego, poza szczególnym typem eksportera serwisu Spring.ServiceModel.ServiceExporter.
Od serwera wymaga się hostowania naszego serwisu i do tego służy klasa ServiceHost. To kolejna zmiana w kodzie jaką musimy wykonać w stosunku do .NET Remoting. Zakładając, że aplikacja hostująca nie jest częścią modelu aplikacji, a więc ta zmiana się nie liczy;).
Teraz klient czyli to najciekawsze z punktu widzenia WCF. Zmiany w kodzie? Nie! Żadnych zmian! Aplikacja działa tak samo, tj. pobiera instancję serwisu z kontenera IoC Spring-a i wywołuje jego metody. Zatem wszystkie zmiany ograniczą się do pliku konfiguracyjnego.
Definiujemy ChannelFactory dla naszego serwisu:


<object id=”helloServiceChannelFactory” type=”System.ServiceModel.ChannelFactory&lt;BeezDev.SpringWCFApplication.Model.IHelloService>, System.ServiceModel”>
  <constructor-arg name=”endpointConfigurationName” value=”tcpEndpoint” />
</object>

Jako parametr generyczny podajemy typ naszego serwisu i kolejna rzecz wymagająca wyjaśnienia to argument konstruktora. Tutaj przekazujemy nazwę endpointa-a z konfiguracji WCF-a. O tym jeszcze wspomnę później.

Następnie definicja naszego serwisu:


<object id=”helloService” type=”BeezDev.SpringWCFApplication.Model.IHelloService, BeezDev.SpringWCFApplication.Model.Interface”
  factory-object=”helloServiceChannelFactory”
  factory-method=”CreateChannel” />

Jako klasę fabrykującą wskazujemy obiekt zdefiniowany wcześniej, a metoda fabrykująca to oczywiście CreateChannel.
To wszystko! No prawie wszystko;). Pozostaje kwestia konfiguracji samego WCF, która jest niejako poza tematem tego artykułu. Wspomnę tylko, że konfiguracji dokonujemy w tradycyjny sposób, tj. w sekcji system.serviceModel pliku app.config. W przykładzie użyłem hostowania serwisów po protokole TCP z binarną serializacją. Po szczegóły związane z cała magią konfiguracji WCF-a tzw. ABC odsyłam do dokumentacji.

Pełne solution tutaj. Miłej zabawy!

Blog na WordPress.com.