Generowanie kodu na przykładzie klas repozytorium, szablonów T4 oraz T4 Toolbox

Wprowadzenie

Wzorzec DAO (Data Access Object) jest jednym z częściej używanych wzorców do organizacji warstwy dostępu do danych . Dla każdego obiektu/tabeli z bazy (np User, Product) tworzymy dedykowany interfejs/klasy. Klasy te często nazywamy repozytoriami* (np. UserRepository, ProductRepository) i na ogół zawierają takie same metody.

Jestem wielkim fanem szablonów T4. Lubie je wykorzystywać do automatyzacji tworzenia powtarzalnych i schematycznych elementów kodu. Właśnie klasy repozytoriów są takimi elementami, które generuje z wykorzystaniem szablonów. W tym wpisie pokaże Ci, jak to zrobić wykorzystując bibliotekę T4 Toolbox (https://github.com/olegsych/T4Toolbox), która bardzo ułatwia tworzenie nowych szablonów.

* – używanie „Repository” w nazwach klas DAO powoduje, że część programistów myli ten wzorzec DAO z wzorcem Repository z DDD. Sam kiedyś byłem taką osobą. Jeden z wątków na stackoverflow (https://stackoverflow.com/questions/8550124/what-is-the-difference-between-dao-and-repository-patterns) pokazuje różnice między tymi dwoma wzorcami. Zachęcam do przejrzenia, gdy nie widzisz różnicy między tymi dwoma wzorcami.

Repozytoria

Jak implementujemy repozytoria? Sposobów jest kilka, co programista to inny 🙂 Jednym z nich jest rozbicie aplikacji na kilka projektów:

  • Models – tutaj dodajemy klasy takie jest Product lub User, które będą przenosić dane między warstwami oraz posłużą do wygenerowania bazy danych na przykład przez Entity Framework
  • Logic – tutaj dodajemy interfejsy repozytoriów dla poszczególnych klas z projektu Models
  • DataAccess – implementacje repozytoriów z wykorzystaniem jakieś biblioteki np. Entity Framework

Screen poniżej pokazuje organizacje z przykładu do wpisu (gdzie repozytoria są już generowane przesz szablony). Zaznaczone fragmenty pokazuje poszczególne elementy organizacji kodu.

t4 repositories soluction structure

W przykładzie wykorzystałem kilka bazowych klas i interfejsów, które są później używane w wygenerowanym kodzie. Jest wiele sposobów na implementacje bazowego repozytorium. Mogą one zawierać różne metody. Poniższa implementacja to tylko przykład takiej implementacji i nie jest on kluczową rzeczą w tym wpisie 🙂

Klasa BaseModel zawiera kilka podstawowych właściwości, które chce mieć w każdym z swoich modeli i później tabel w bazie danych. W przykładzie posłuży przede wszystkim po to, aby można było napisać kod repozytoriów w sposób generyczny. Właściwość Id jest kluczem głównym, którą też wykorzystamy.

Generyczny interfejs IRepository jest bazowym interfejsem dla poszczególnych interfejsów repozytoriów (np. IUserRepository, IProductRepository). Zawiera on podstawowe metody, które chcemy mieć w każdym repozytorium. Jak pisałeś wcześniej można znaleźć wiele różnych implementacji metod w repozytorium.

Klasa Repository jest bazową implementacją IRepository i zostanie wykorzystana jaka klasa bazowa dla generowanych klas repozytoriów.

Większość kodu repozytoriów będzie w tych dwóch elementach. Można by ten kod również wrzucać do generowanych klas, ale takie podejście na dłuższą metę jest mniej wygodne. Edytor szablonów T4 jest bardzo ubogi, nie ma za bardzo podpowiedzi oraz informacji, czy wynikowy kod będzie się kompilował, dlatego najlepiej jak na więcej kodu wrzucać to takich wspólnych klas lub interfejsów, a w samym szablonie pisać jak najmniej kodu.

T4 Toolbox

Szablony T4 możemy dodać do projektu na różne sposoby. Niestety czyste szablony dostępne w Visual Studio są bardzo toporne w użyciu i warto skorzystać, z czegoś co ułatwi ich tworzenie. Jednym z takich narzędzi jest T4 Toolbox.

W pierwszej kolejności należy zainstalować dodatek z marketplace (https://marketplace.visualstudio.com/search?term=T4Toolbox&target=VS&category=All%20categories&vsVersion=&sortBy=Relevance) dla odpowiedniej wersji Visual Studio.

Po instalacji pojawi się nowa kategoria w dodaniu nowego elementu do projektu:

t4 repositories new item

Do generowania kodu T4 Toolbox wykorzystuje trzy elementy:

  • Template – szablon, z którego zostanie wygenerowany plik
  • Generator – logika wykorzystywania szablonów
  • Script – właściwy szablon, który wygeneruje kod – korzysta z generatora

T4 Toolbox – Template

Szablon z T4 Toolbox służy do zdefiniowania struktury wygenerowanego pliku. Z jednej strony jest to zwykła klasa C#, które może zawierać to samo co w normalnym kodzie (w przykładzie wykorzystamy pole). Z drugiej strony zawiera specjalną metodę TransformText, która w sobie zawiera szablon generowanego pliku.

Z wszystkich plików tt jest on najbardziej skomplikowany. Wszystko przez to, że w tym pliku miesza nam się kod C# samej klasy szablonu z generowanym kodem C#. Przez co bardzo łatwo o popełnienie błędu, szczególnie, że Visual Studio za bardzo nie pomaga w edycji pliku tt. Jedynie co, to kod klasy szablony ma inne tło (szare w jasnej skórce Visual Studio) od generowane kodu (białe tło):

t4 repositories visual studio support

W szablonach T4 sama składnia jest relatywnie prosta. Najczęściej używa się:

  • <#@ [jakaś dyrektywa] #> – dyrektywy na początku pliku np. import innego szablonu, namespace itp
  • <#+ … #> – definicja klasy lub elementu klasy szablonu
  • <#= …. #> – przekazanie jakiegoś napisu z klasy szablonu do generowanego pliku
  • <# … #> – fragment kodu szablonu w generowanym kodzie – przydatne przy ifach lub pętlach

IRepositoryTemplate

Przeanalizujmy teraz szablon T4 Toolbox, który wykorzystamy do generowania interfejsów dla repozytoriów (dla czytelności możesz również analizować kod szablonu z screena powyżej, gdzie jest kolorowanie z Visual Studio – niestety gist nie wyświetla dobrze szablonów plików tt):

W szablonie mamy zdefiniowaną klasę IRepositoryTemplate, które zawiera jedno pole ClassName. Posłuży nam ono do przekazania z generatora informacji dla jakiej klasy generujemy interfejs repozytorium.

W metodzie TransformText znajduje się generowany kod. T4 Toolbox udostępnia kilka pomocniczym metod i właściwości, które możemy wykorzystać, aby tworzenie szablonu było łatwiejsze:

  • DefaultNamespace – poprawny namespace uwzględniający lokalizację generowanego pliku – nie trzeba tego ręcznie ustawiać
  • Identifier – zwraca poprawną nazwę uwzględniającą reguły C# dla przekazanego string – np. usuwa spacje

Generowany interfejs jest prosty: w nazwie używamy przekazaną z generatora nazwę klasy, podobnie używamy tej nazwy w parametrze generycznym w IRepository.

Warto zauważyć, że generowany interfejs jest typu partial. Jest to bardzo ważne, ponieważ w generowanym kodzie nie możemy nic zmienić, w szczególności dodać nowych metody. Przy każdym generowaniu byłyby one nadpisane. Dlatego interfejs jest typu partial, przez co później w dodatkowym pliku możemy dodać metody specyficzne dla danego repozytorium. W przykładzie na githubie w przypadku IUserRepository jest właśnie dodany takich dodatkowy plik z specyficzną metodą. Zachęca do przejrzenia przykładu.

T4 Toolbox – Generator

Kolejnym elementem z T4 Toolbox jest generator. W nim wrzucamy kod, który wykorzysta wcześniej przygotowane szablony T4 Toolbox. Jeden szablon T4 Toolbox generuje jeden plik wynikowy, natomiast w generatorze możemy wykorzystać wiele różnych szablonów (raz lub wiele razy). W naszym przypadku w generatorze będziemy mieli informacja dla jakich klas chcemy wygenerować interfejsy repozytoriów. Następnie w pętli wykorzystamy IRepositoryTemplate do wygenerowania poszczególnych interfejsów.

W pierwszej kolejności za pomocą dyrektywy include importujemy szablon IRepositoryTemplate.tt. Musimy to zrobić dla każdego szablonu, który chcemy wykorzystać w generatorze. Dla zaimportowanego szablonu tworzymy pole w klasie generatora (może to być również zmienna w metodzie), które niżej wykorzystujemy do generowania interfejsów.

W generatorze kluczowa jest metoda RunCore. Ona jest uruchamiana podczas generowania kodu. W niej mamy tablicę, w której są nazwy klas, dla których chcemy wygenerować repozytoria. W przykładzie jest to ustawione ręcznie. Można się też pokusić o coś bardziej generycznego. Pokaże to w jednym w kolejnych wpisów.

W pętli korzystamy z wcześniej zaimportowanego szablonem. Ustawiany nazwę klasy i za pomocą metody RenderToFile generujemy interfejs dla klasy modelu. Do metody przekazujemy nazwę wygenerowanego pliku. W nazwie korzystam z słowa „generated”. Jest to dla mnie później informacja, że ten plik został wygenerowany przez szablony i wszystkie w nim zmiany zostaną wcześniej, czy później usunięte.

T4 Toolbox – Script

Ostatnim elementem całej układanki jest plik skryptu. Jest to wykonywany element. Jego zapis powoduje uruchomienie szablonu T4 i wygenerowanie kodu. Zapis szablonu i generatora nie powoduje generowania. Kod w skrypcie jest bardzo prosty. Importujemy plik tt generatora. Tworzy jego instancję i na końcu wywołuje metodę Run:

Zapisane pliku spowoduje wygenerowania interfejsów repozytoriów dla klasy User oraz Product. Zrzut ekranu z początku wpisu pokazuje jak będzie wyglądało solution po wygenerowaniu tych dwóch interfejsów. Sam wygenerowany plik dla klasy User będzie wyglądał tak:

Klasy repozytoriów

Generowanie klas repozytoriów jest bardzo podobne do generowania interfejsów. Również tworzymy szablon, generator oraz skrypt. Zawartość poszczególnych plików jest bardzo zbliżona. Dlatego poniżej znajduje się ich zawartość. Nie będę ich opisywał. Zachęcam do własnej analizy 🙂 Zadaj pytanie jak byłoby coś niejasnego.

Przykład

Tradycyjnie na githubie (https://github.com/danielplawgo/T4Repositories) jest przykład. Znajdują się w nim wszystkie szablony z wpisu. Dodatkowo stworzyłem aplikacje konsolową, która pokazuje, że wygenerowane interfejsy oraz klasy faktycznie działają. Aby uruchomić aplikację wystarczy tylko w app.config ustawić poprawny connection string do bazy (struktura bazy wygeneruje się sama podczas pierwszego uruchomienia aplikacji).

Podsumowanie

Generowanie kodu nie musi być skomplikowane. T4 Toolbox bardzo ułatwia wykorzystywanie szablonów T4 w projekcie. Dzięki podzieleniu logiki generowania kodu na trzy elementy jest to dużo łatwiejsze do ogarnięcia.

Warto się zastanowić, czy gra jest warta świeczki? Z jednej strony można zauważyć, że generowane interfejsy/klasy nie są skomplikowane i ich ręcznie dodane nie jest czasochłonne. Szczególnie w powyższej implementacji, gdy i tak trzeba zmodyfikować generator, aby dodać nową klasę do tablicy.

Z drugiej strony taka inwestycja z czasem bardzo szybko się zwraca. W szczególności, gdy mamy sporo klas modeli (np. wygenerowanych na podstawie bazy danych). Wtedy takie szablony bardzo ułatwiają dodanie repozytoriów do projektu.

Dodatkowo mając taki szablon, dużo łatwiej jest w przyszłości modyfikować kod repozytoriów. Przykładowo w powyższej implementacji DataContext jest przekazywany jako Lazy w konstruktorze. Gdybyśmy z jakiegoś powodu chcieli to zmienić, to wystarczy tylko zmienić RepositoryTemplate i wygenerować od nowa wszystkie klasy. Bez wątpienia będzie to dużo szybsze rozwiązanie niż ręczna edycja każdego repozytorium.

W swoich projektach staram się jak najwięcej używać szablonów. Z czasem jest to już tylko kopiuj-wklej z innych projektów, gdzie po drobnej zmianie mogę korzystać z dobrodziejstw szablonów.

Ciebie również zachęcam do używania szablonów T4 w swoich projektach.

 

1 thought on “Generowanie kodu na przykładzie klas repozytorium, szablonów T4 oraz T4 Toolbox

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *