Technologie .NET - laboratorium 3
Post

Technologie .NET - laboratorium 3

Wstęp i opis

Podczas laboratoriów zaimplementowana zostania logika repozytorium z wykorzystaniem EF Core 6.0 dla encji Account, Apartament, Image, Landlord oraz Tenant.

Implementacja repozytorium

Na poprzednich zajęciach został zaimplementowany kontekst bazodanowy, za pomocą którego jesteśmy w stanie wygenerować nową bazę danych oraz zarządzać jej stanem za pomocą narzędzia migracyjnego dostępnego w EF Core. Następnym krokiem jest stworzenie logiki, której zadaniem jest operowanie nad danymi w powstałych tabelach.

Przed przejściem do implementacji repozytorium, upewnij się że folder Entites jest w projekcie ApartmentRental.Infrastructure , jeśli nie - przenieś go i zaktualizuj odpowiednio namespace’s w każdej klasie.

Prace nad naszym pierwszym repozytorium zaczniemy od stworzenia prostego generycznego interfejsu IRepository<T>, którego zadaniem będzie wskazanie takich podstawowych funkcjonalności jak:

  • Pobranie wszystkich rekordów i informacji o danej encji o typie kolekcji IEnumerable. Kolekcja ta udostępnia enumerator obsługujący prostą iterację - GetAll()
  • Pobranie jednego rekordu encji za pomocą identyfikatora - GetById(int id)
  • Dodanie nowego rekordu do tabeli - Add(T entity)
  • Modyfikacja wskazanej encji bazodanowej - Update(T entity)
  • Skasowanie encji za pomocą identyfikatora - DeleteById(int Id)

Jednym z naszych głównych wymogów co do aplikacji jest zapewnienie że nasze rozwiązanie będzie działało asynchronicznie, czyli zadania będą działały na wielu wątkach, co przekładać się będzie na znaczną wydajność naszego serwisu. W tym celu metody w naszym interfejsie będą zwracały klasę Task , która to reprezentuje pojedynczą operację wykonywaną asynchronicznie w wątku puli wątków.

W projekcie ApartmentRental.Infrastructure, dodajmy folder Repository. W tym folderze stwórzmy wspomniany interfejs.

Desktop View

Przejdźmy do implementacji interfejsu IRepository<T>. Na pierwszą klasę z implementacją repozytorium wybierzmy klasę Apartment. Aby zapewnić poprawne wiązanie podczas wstrzykiwania zależności podczas rejestracji serwisów i ich implementacji w cyklu życia aplikacji oraz aby zapewnić rozszerzenie funkcjonalności repozytorium dla danej klasy, stworzymy interfejs IApartmentRepository który będzie rozszerzeniem IRepository<T>. Jako że IRepository jest interfejsem generycznym oraz operujemy na domenie Apartament, to typem argumentu przekazywanym do interfejsu będzie typ Apartment. Dodajmy również zwykłą klasę ApartmentRepository, która będzie implementowała IApartmentRepository. Aby wygenerować pustą implementację metod wskazanych w interfejsie, w środowisku programistycznym JetBrains Rider należy wykonać kombinację klawiszy ctrl+i i zaznaczyć wszystkie brakujące metody.

Desktop View Desktop View Desktop View Desktop View

Przed rozpoczęciem implementacji metod dodajmy do klasy stworzony na poprzednich zajęciach kontekst bazodanowy. Odwołamy się w tym celu do klasy MainContext , ustawmy jej właściwości jako klasę prywatną tylko do odczytu (private readonly) oraz napiszmy konstruktor repozytorium z inicjalizacja tej zmiennej.

Desktop View

Pierwszą rzeczą którą zaimplementujemy, będzie funkcjonalność pobierania wszystkich dostępnych apartamentów. Implementacja metody GetAll() jest prosta, jednakże jest w niej haczyk. Aby pobrać pełną listę (dla potrzeb laboratorium zakładamy że nasza baza danych nie posiada milion+ encji o apartamentach, gdzie pobranie wszystkich naraz mogłoby zabić nasz mikroserwis) wystarczy odnieść się do kontekstu bazodanowego, a w nim wybrać odpowiednią encję oraz wywołać metodę ToListAsync() ( w C#, konwencją jest dodawanie suffixu Async do każdej asynchronicznej metody). Wspomnianym haczykiem jest tzw. lazy loading, który ze wspomnianych względów optymalizacji aplikacji, pobiera tylko i wyłącznie niezagęszczone dane encji np. nie dostaniemy informacji o adresie danego apartamentu. W takim wypadku, możemy zmienić rodzaj pobierania danych z bazy. Rozróżniamy tutaj dwa kolejne typy: explicit oraz eager. Pierwszy z nich pobiera dane w locie, natomiast drugi polega na pobraniu wszystkich zagęszczonych danych odrazu. Skorzystajmy w takim razie z pobierania explicit. Aby użyć tego typu pobierania, musimy dla każdej wyciągniętej encji wskazać odpowiednią referencję do danej właściwości encji np. wspomnianego adresu oraz ją załadować. Należy zwrócić jeszcze uwagę na kolejną bardzo ważną rzecz. W niektórych przypadkach kod asynchroniczny musi zostać zsynchronizowany. Przypadkiem wymaganej synchronizacji może być próba dodania nowego użytkownika do tabeli. Przed dodaniem danego użytkownika, chcielibyśmy zweryfikować czy użytkownik o jakiś właściwościach już przypadkiem nie istnieje. Przyjmijmy jeszcze że mamy dwie metody, pierwszą która sprawdza czy użytkownik istnieje i drugą która dany profil tworzy. Bez punktu synchronizacji, metoda próbująca stworzyć nowego użytkownika, mogłaby się wykonać przed metodą sprawdzenia (obie metody działały by na innych wątkach). Rozwiązaniem tego problemu jest użycie słowa kluczowego await przed każdą metodą asynchroniczną. Jest ona wspomnianym punktem synchronizującym i dodanie jej przed hipotetycznymi metodami sprawi że druga funkcja nie wykona się przed pierwszą. Należy również dodać że await , nie blokuje wątków, przez co aplikacja nie zatrzymuje się w danym punkcie, tylko zasób jest zwalniany do wykonania innych zadań dopóki “z awaitowana” metoda nie zwróci wyniku. Wróćmy teraz do naszego zadania. Dodajmy słowo kluczowe await w miejscach których pobieramy listę apartamentów oraz tam gdzie ładujemy referencję do adresu. Każda metoda w której używamy słowa await musi poprzedzać wskazanie typu słowem async. Implementacja metody GetAll() powinna teraz wyglądać następująco:

Desktop View

Zaimplementujmy drugą metodą repozytorium - pobieranie jednej encji po identyfikatorze. Logika metody jest zbliżona do poprzedniej. Zamiast użycia metody kontekstu ToListAsync() użyjemy metody SingleOrDefaultAsync() , która zwraca unikalną pojedynczą encje lub jeśli taka nie istnieje (lub jeśli nie jest unikalna, czyli istnieje ich wiele) domyślną wartość (dla obiektów - null). Korzystając z kwerend LINQ, jesteśmy w stanie szybko wyfiltrować kolekcję po zadanym atrybucie. Możemy użyć metodę Where() lub użyć predykatu w metodzie SingleOrDefaultAsync(). W przypadku otrzymania nulla, który jest przez nas sytuacją niepożądaną - powinniśmy rzucić wyjątkiem. Stwórzmy swój własny wyjątek i nazwijmy go EntityNotFoundException. W projekcie Infrastructure stwórzmy folder Exceptions a w nim klasę EntityNotFoundException, która powinna dziedziczyć po standardowym typie Exception. Stworzony wyjątek wywołajmy w momencie kiedy wyciągnięta encja będzie nullem. Cały kod, wygląda następująco:

Desktop View Desktop View

Przejdźmy do metody dodawania nowych apartamentów. Za pomocą kontekstu wywołujemy encję Apartment. Ustawmy również jeszcze pole informujące nas o stworzeniu nowej encji. Przypiszmy właściwości DateOfCreation w obiekcie Apartments, bieżący czas przy pomocy DateTime.UtcNow()(czas w formacie UTC jest uniwersalny i powinno się go stosować jako standard by nie dopuścić do sytuacji rozbieżności danych a lokalnymi ustawieniami czasu serwera). By dokonać zapisu należy wykonać metody AddAsync() oraz SaveChangesAsync().

Desktop View

Przedostatnią metodą do implementacji jest DeleteById(). Funkcja przed usunięciem danego rekordu powinnna sprawdzić czy żądany obiekt do usunięcia w ogóle znajduje się w tabeli, jeśli ni e- wywołamy stworzony wcześniej wyjątek. Jeśli tak, to korzystając z metody Remove() usuniemy obiekt, a zmiany w kontekście wykonamy za pomocą metody SaveChangesAsync().

Desktop View

Ostatnią metodą repozytorium która została do zaimplementowania, jest funkcja aktualizacji danych danej encji. W pierwszej kolejności sprawdzamy czy obiekt który chcemy zaktualizować w ogóle występuje w naszej bazie danych. Jeśli nie, rzućmy standardowo stworzonym poprzednio przez nas wyjątkiem. Kod aktualizacji danych prezentuje się następująco:

Desktop View

Zadania:

  1. Dodaj do nazwy każdej powstałej metody asynchronicznej suffix Async
  2. Do metody dodawania nowej encji Apartamentu dodaj walidację czy obiekt o takim samym adresie już nie istnieje w bazie danych. Jeśli istnieje wywołaj odpowiedni wyjątek.
  3. Dodaj brakujące repozytoria dla pozostałych encji bazodanowych (LandLord, Tenant, Address, Images)
  4. Zacommituj I wypushuj zmiany na swoje konto na githubie i powiadom prowadzącego o wykonaniu zadania. W wiadomości uwzględnij link do repozytorium.