Generyczne Repozytoria i Unit of Work

By | March 12, 2016

W swoim projekcie będę operował na kilku różnych typach encji, które przy zastosowaniu wzorca repozytorium jaki przedstawiłem wcześniej wymagałyby każdorazowo oddzielnej klasy repozytorium. Chociaż wszystkie implementowałyby ten sam interfejs i oferowały w większości tą samą funkcjonalność, to wymagałyby wielokrotnego powtarzania tych samych linijek kodu. Byłoby to stosunkowo proste, ale nie miałoby większego sensu. Potrzebowałem rozwiązania, które będzie operowało na każdym obiekcie jaki sobie wymyślę i które będzie wystarczająco elastyczne, żeby zapewnić mi możliwość chociażby sortowania/filtrowania według różnych kryteriów.

Klasy generyczne

Zacznę od wyjaśnienia, czym są klasy generyczne. Trochę więcej będzie można poczytać o nich w jednym z linków, które umieszczę na końcu posta, ja ograniczę się do informacji ogólnych. Na samym początku polecam Wam do zajrzenia w kod źródłowy klasy, z której na pewno korzystacie na co dzień. Mowa o klasie List. Zauważcie, że metody tej klasy, z których korzystacie na co dzień nie przyjmują intów, stringów i innych obiektów, a jedynie T

Jeśli się nad tym zastanawialiście, to wiecie, że nie ma oddzielnych klas List<string>, List<int> itd. Chociaż używamy tego ciągu znaków całkiem często, to w rzeczywistości jest to klasa List<T>, w której T oznacza możliwość przyjmowania każdego typu. Dodam, że T jest wyłącznie przyjętą konwencją, bo równie dobrze w nawiasach moglibyśmy umieścić List<Type>, List<X> lub Dictionary<TKey,TValue>.

Klasę generyczną możemy określić w następujący sposób:

Linia ta oznacza dokładnie to, że tworzymy publiczną klasę o nazwie GenericRepository<T>, która jest implementacją interfejsu IRepository<T>, gdzie T musi być klasą, implementującą interfejs IEntity. Pozwoliłem sobie na pogrubienie elementu, który określa, z jakimi obiektami chcemy pracować w naszej klasie. Nie musimy tego robić i tak jak w przypadku List możemy przyjąć wszystko, ale w przypadku repozytorium potrzebowałem wykonywać pewne operacje na właściwości ID, obiektów. Musiałem więc zagwarantować swojemu generycznemu repozytorium, że każdy obsługiwany przez nie obiekt, będzie zawierał ID. Zrobiłem to za pośrednictwem bardzo prostego interfejsu.

 

Repozytorium generyczne

Moje repozytorium jest całkiem elastyczne, chociaż ma swoje wady i problemy, z czego jeden jest bardzo istotny, ale o nim napiszę pod sam koniec posta, a samo rozwiązanie przedstawię za kilka dni. Repozytorium przejrzymy metoda po metodzie i jest implementacją następującego interfejsu.

Zacznijmy od początku klasy:

 

W konstruktorze przyjmujemy kontekst bazy danych, który przekazywany jest do repozytorium przez Unit of Work. Na bazie otrzymanego kontekstu, określamy DbSet, który będzie odpowiedni dla przyjmowanego przez repozytorium typu danych. Dzięki temu w repozytorium mamy dwie referencje. Jedną do DbContext, z której czasem będziemy zmuszeni skorzystać oraz do DbSet, która ułatwi nam wykonywanie prostych operacji CRUD.

 

Pierwszą metodą jest SelectAll. Zdecydowałem, że nie będzie to prosty return dbSet, który zawsze będzie zwracał wszystkie obiekty z tabeli. Metoda przyjmuje predykat, który ma domyślną wartość null przy której użyciu zwraca nam wszystko. Jeśli jednak będziemy mieli chęć wykonać proste filtrowanie i zwrócić na przykład produkty, które należą do jednej kategorii, to nic nie stoi na przeszkodzie byśmy skorzystali z metody z prostą lambdą w na przykład taki sposób SelectAll(x => x.Category == category). Rozwiązanie to będzie miało zastosowanie w przyszłości i jest wystarczająco elastyczne, by sprawdziło się w przypadku każdego obiektu jaki przyjmie repozytorium.

Na bardzo podobnej zasadzie oparłem metodę Select, w której również zrezygnowałem z popularnego rozwiązania wyszukiwania po ID na rzecz predykatu, który pozwala nam na praktycznie dowolne kryteria filtrowania wyniku.

 

O ile metoda Add nie jest niczym szczególnym, to w Update nie udało mi się znaleźć żadnej możliwości, która pozwalałaby na skorzystanie z DbSetu. W związku z tym byłem zmuszony użyć metod klasy DbContext by przypiąć przekazywaną encję i zmienić jej stan na EntityState.Modified. Dzięki temu Entity Framework “wie”, że nastąpiły jakieś zmiany i należy poprawić pewne wartości w bazie. Niestety działa to tylko w przypadku prostych obiektów, bo jeśli typ, który obsługuje dana instancja repozytorium zawiera kolekcje i relacje wiele-wiele, to modyfikacja zawartości tej kolekcji nie zostanie zaktualizowana. Problem ten nie występuje w Entity Framework bez użycia repozytorium i wymaga podejścia jak przy korzystaniu z DbContext z wyłączonym Lazy Loading. Więcej szczegółów znajdziecie w jednym z linków na końcu, a moje rozwiązanie problemu poznacie za kilka dni.

Obie metody Delete to absolutny standard. Lubię zwracać usunięty z bazy obiekt w celu wyświetlania powiadomień lub przechowania obiektu na wypadek, gdybyśmy chcieli cofnąć zmianę. Póki co nie robiłem jeszcze tej funkcjonalności, więc opowiem o niej w bliżej nieokreślonej przyszłości.

 

Unit of Work

UoW przy korzystaniu z repozytoriów generycznych ma odrobinę większą rolę, niż ten o którym pisałem ostatnio. Nie służy on już tylko do przechowywania innych repozytoriów. Mój UoW jest implementacją tego interfejsu:

I po raz kolejny omówimy je metoda po metodzie.

 

Ważnym elementem tej implementacji jest słownik. Przechowuje on instancje repozytoriów, by uniknąć występowania kilku różnych repozytoriów dla tego samego typu. Cała magia dzieje się w następnej metodzie.

 

Jest ona o wiele prostsza niż mogłoby się wydawać. Po wywołaniu repozytorium dla określonego typu Unit of Work sprawdza czy w jego słowniku nie znajduje się już jego aktywna instancja. Jeśli słownik zawiera wśród kluczy dany typ, to zwraca tą instancję jako IRepository danego typu. W innym przypadku tworzona jest nowa instancja, dodawana jest do słownika i zwracana.

 

Metoda Save to już czysta formalność.

 

Pytanie tylko jak w takim razie wywołujemy generyczne repozytorium z poziomu na przykład kontrolera? Wywołanie jest stosunkowo proste, chociaż musiałem się do niego chwilę przyzwyczaić.

W przypadku metod z predykatem, możemy w nie wpisać co nam przyjdzie do głowy. Na przykład wyszukiwanie po ID.

 

Chociaż nie zachęcam do korzystania z repozytorium z ORM, to sama implementacja naprawdę mi się podoba. Naprawdę duże wrażenie zrobiła na mnie elastyczność, w porównaniu do standardowych repozytoriów. Niestety Zastosowanie tego rozwiązania z Entity Framework odcięło mnie od bezpośredniego korzystania z możliwości oferowanych przez DbContext i DbSet, więc natknąłem się na jeden dość poważny problem, o którym napiszę za kilka dni.

 

Kilka linków: