From 2cd414baa6f2edc612c6c3e5e9ea6391a05f38aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Wasilewski-CL?= Date: Fri, 26 Jan 2024 06:40:31 +0100 Subject: [PATCH 1/7] ADD Dapper --- README.md | 733 +++++++++++++++++- TaskManager.Tests/MockRepository.cs | 46 ++ .../{TaskTests.cs => TaskItemTests.cs} | 68 +- TaskManager.Tests/TaskManagerServiceTests.cs | 151 ++-- TaskManager/BusinessLogic/DapperExtensions.cs | 21 + TaskManager/BusinessLogic/IRepository.cs | 15 + TaskManager/BusinessLogic/Repository.cs | 165 ++++ .../BusinessLogic/{Task.cs => TaskItem.cs} | 37 +- .../{TaskStatus.cs => TaskItemStatus.cs} | 2 +- .../BusinessLogic/TaskManagerService.cs | 81 +- TaskManager/BusinessLogic/User.cs | 20 + TaskManager/Program.cs | 142 +++- TaskManager/TaskManager.csproj | 5 + 13 files changed, 1320 insertions(+), 166 deletions(-) create mode 100644 TaskManager.Tests/MockRepository.cs rename TaskManager.Tests/{TaskTests.cs => TaskItemTests.cs} (56%) create mode 100644 TaskManager/BusinessLogic/DapperExtensions.cs create mode 100644 TaskManager/BusinessLogic/IRepository.cs create mode 100644 TaskManager/BusinessLogic/Repository.cs rename TaskManager/BusinessLogic/{Task.cs => TaskItem.cs} (54%) rename TaskManager/BusinessLogic/{TaskStatus.cs => TaskItemStatus.cs} (75%) create mode 100644 TaskManager/BusinessLogic/User.cs diff --git a/README.md b/README.md index 32addd9..e584876 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,727 @@ ![Coders-Lab-1920px-no-background](https://user-images.githubusercontent.com/30623667/104709387-2b7ac180-571f-11eb-9b94-517aa6d501c9.png) -# Kilka ważnych informacji +> Jest to gotowa - wzorcowa wersja warsztatu TaskManager z Dapper. -Przed przystąpieniem do rozwiązywania zadań przeczytaj poniższe wskazówki +## Wstęp do warsztatu TaskManager z Dapper -## Jak zacząć? +Celem dzisiejszego warsztatu jest rozbudowa aplikacji TaskManager. Jest to aplikacja do zarządzania i planowania zadań do wykonania. Aplikacja będzie składała się z pięciu części: +- logiki biznesowej (modele), +- aplikacji (serwis), +- bazy danych, +- testów, +- interfejsu użytkownika (w postaci konsoli). -1. Stwórz [*fork*](https://guides.github.com/activities/forking/) repozytorium z zadaniami. -2. Sklonuj fork repozytorium (stworzony w punkcie 1) na swój komputer. Użyj do tego komendy `git clone adres_repozytorium` -Adres możesz znaleźć na stronie forka repozytorium po naciśnięciu w guzik "Clone or download". -3. Rozwiąż zadania i skomituj zmiany do swojego repozytorium. Użyj do tego komend `git add nazwa_pliku`. -Jeżeli chcesz dodać wszystkie zmienione pliki użyj `git add .` -Pamiętaj że kropka na końcu jest ważna! -Następnie skommituj zmiany komendą `git commit -m "nazwa_commita"` -4. Wypchnij zmiany do swojego repozytorium na GitHubie. Użyj do tego komendy `git push origin master` -5. Stwórz [*pull request*](https://help.github.com/articles/creating-a-pull-request) do oryginalnego repozytorium, gdy skończysz wszystkie zadania. +Podczas warsztatów rozbudujesz projekt o nowe funkcjonalności oraz integrację z bazą danych przy pomocy paczki NuGet `Dapper`. -Poszczególne zadania rozwiązuj w odpowiednich plikach. +Pamiętaj, że programista to nie jest "zwykły klepacz kodu" tylko pracujący kreatywnie rzemieślnik i inżynier dbający o logikę działania aplikacji oraz jej całą konstrukcję. Aby dobrze wykonywać swoją pracę należy używać dobrych praktyk progamistycznych oraz projektowych. Podział projektu na warstwy ze względu na odpowiedzialność kodu jest jedną z takich praktyk. Współcześnie projektuje się tzw. aplikacje N-wartstwowe. -### Poszczególne zadania rozwiązuj w odpowiednich plikach. +Z tego powodu dodatkowym celem warsztatów jest podział aplikacji na warstwy wg odpowiedzialności. Docelowo aplikacja na koniec warsztatów będzie składała się z czterech warstw: +- warstwy logiki biznesowej (modele), +- warstwy aplikacji (serwis), +- warstwy prezentacji (konsola), +- warstwy dostępu do danych (repozytorium). -**Repozytorium z ćwiczeniami zostanie usunięte 2 tygodnie po zakończeniu kursu. Spowoduje to też usunięcie wszystkich forków, które są zrobione z tego repozytorium.** +Idea działania jest następująca: +- użytkownik korzysta z warstwy prezentacji (konsola), +- program konsolowy korzysta z warstwy aplikacji (serwis), +- serwis korzysta z warstwy logiki biznesowej (modeli) i dostępu do danych (repozytorium), +- logika biznesowa w modelach jest główną częścią "biznesu", +- repozytorium korzysta z bazy danych do przechowywania stanu model biznesowych. +Dzięki modularnej budowie aplikacja staje się niezależna od swoich modułów/klocków. Wyobraź sobie sytuację, w której należy wymienić bazę danych na inną. Wówczas nie musisz zmieniać całej aplikacji. Wystarczy przepiąć moduł dostępu do danych, a pozostałe warstwy korzystające z niego nawet tego nie zauważą :) Co więcej w dalszej części kursu możesz wykorzystać obecny projekt to tego, aby użyć innej warstwy prezentacji danych i zamiast konsoli komunikować się poprzez API, które potem będzie komunikowało się ze stroną internetową. -## Warsztat TaskManager +Dzięki sprytnemu zabiegowi i podziału kodu na mniejsze części programista jest gotowy na bezgraniczny rozwój aplikacji. Dokonywanie modyfikacji w aplikacji będzie łatwiejsze, a co najważniejsze, będzie można dzielić się pracą z innymi programistami tak, aby nie mieszać swoich wersji kodu. -Jest to gotowa - wzorcowa wersja warsztatu TaskManager. +W kolejnych artykułach dowiesz się jakie są wymogi działania aplikacji. + +### Czego nauczysz się podczas tego warsztatu? + +Warsztat jest w formie wykonania jednego dużego zadania jakim jest rozbudowa projektu o integrację z bazą danych. Nauczysz się jak rozwijać swoją aplikację, porządkować ją i rozbudowywać o kolejne moduły i funkcje. Na pewno da to duży zastrzyk praktycznej wiedzy i pozwala na szybsze i bardziej pewne poruszanie się po narzędziach deweloperskich i kodzie C#. + +Nauczysz się podczas warsztatów, że rozbudowa aplikacji wymaga czasami dostosowania jej działania do nowych wymogów i potrzeb, a to wiąże się z procesem tzw. refaktoru kodu. Dzięki temu poczujesz namiastkę pracy programisty w firmie. + +W projekcie tym użyjesz praktycznie wszystkich rzeczy, o których mówiliśmy podczas tego modułu takie jak: + - baza danych MS SQL Server, + - tworzenie zapytań SQL, + - użycie mikro ORM Dapper. + +Wszystko to będzie możliwe do zastosowania w tym projekcie! To na pewno ugruntuje Twoją wiedzę. + +--- +--- +--- +--- + +## Zakres funkcjonalności logiki biznesowej aplikacji TaskManager z Dapper + +**Rozszerzone założenia logiki biznesowej:** +- System umożliwia pobieranie użytkowników z zadaniami. +- Użytkownik posiada cechy: ID, nazwę, lista zadań. System posiada predefiniowanych użytkowników. +- Zadanie posiada dodatkowe pola: twórca zadania (jedna osoba) i osoba przypisana do zadania (jedna osoba). +- System pozwala na zarządzanie zadaniami: przypisywanie zadania do użytkownika. +- System umożliwia przechowywanie w bazie danych informacji o zadaniach, użytkownikach i ich relacji (powiązaniu). +- Jedno zadanie może mieć wyłącznie jednego przypisanego użytkownika. +- Jeden użytkownik może posiadać wiele przypisanych do siebie zadań. + +> Pamiętaj, aby wszystkie metody komunikujące się z bazą danych były wywoływane asynchronicznie (`async`/`await`). + +--- + +### **1. Zmiana nazw typów (refaktor nazw)** + +Przed przystąpieniem do rozbudowy aplikacji warto przeznaczyć czas na dostosowanie i uporządkowanie aplikacji, czyli na refaktor. + +Aplikacja posiada kilka nazw tj. `Task`, `TaskStatus`, które tworzą konflikt nazw z systemowymi zadaniami związanymi z programowaniem asynchronicznym. W trakcie warsztatów będziemy używali tych samych nazw dla typu naszego zadania, jak i zadania w rozumieniu asynchroniczności. Najlepiej rozwiązać ten problem zmieniając nazwę naszego zadania i statusu. + +**Jeżeli w poprzednich warsztatach wybrano inne nazwy dla tych dwóch typów, to możesz pominąć tę część.** + +Sugerujemy, aby nasze zadanie miało nazwę `TaskItem`. Do refaktoru nazw użyjemy polecenia `Rename` (skrót `F2` lub `CTRL+R+R`). Ułatwi to dostosowanie kodu, ponieważ IDE programistyczne automatycznie powinno zmienić nazwę klasy i pliku oraz użycia klasy jak i zmienne. + +**Refaktor `Program`:** +- Zmień metodę `Main` tak, aby była asynchroniczna. Zmień `void` na `async Task`. + +**Refaktor `Task`:** +- Zmień nazwę klasy `Task` na `TaskItem`. + +**Refaktor `TaskStatus`:** +- Zmień nazwę enum `TaskStatus` na `TaskItemStatus`. + +**Refaktor `TaskTests`:** +- Zmień nazwę klasy `TaskTests` na `TaskItemTests`. + +**Refaktor `TaskManagerService`:** +- Wprowadź asynchroniczność do każdej metody. Zmień nazwę każdej metody dodając sufiks `Async` (użyj polecenia `Rename`). Dodaj słowo kluczowe `async` oraz wykorzystaj `Task`, a jeżeli metoda zwraca wartość to użyj `Task<>`. +- Wyszukaj każde wywołanie tych metod i dopisz obsługę `await` przy wywołaniu i upewnij się, że metoda wywołująca również jest asynchroniczna. Dostosuj wywołanie w klasach `TaskManagerService`, `TaskManagerServiceTests` oraz `Program`. + +> Możesz wykorzystać w IDE polecenia do globalnego szukania tekstu `Find all` (skrót `CTRL+SHIFT+F`), aby poszukać tekst `Async(`. IDE wyświetli wszystkie miejsca, gdzie jest użyt ten tekst. Przejdź przez listę i dostosuj wywołania metod. + +**Weryfikacja:** +- Po refaktorze należy zweryfikować poprawność działania aplikacji. +- W pierwszej kolejności przebuduj solucję, aby sprawdzić, czy nie ma błędów kompilacji. Jeżeli jakieś błędy wystąpią, zbadaj je i rozwiąż. Pamiętaj, że możesz sprawdzić w internecie co mogą oznaczać błędy, a także możesz zapytać na Slacku. +- Następnie uruchom wszystkie testy jednostkowe `TaskItemTests` i `TaskManagerServiceTests`. Wszystkie testy powinny przechodzić (mieć zielony kolor). Jeżeli testy dają błędny wynik, to sprawdź dlaczego i napraw testy. Jeżeli metoda testowa do sprawdzania autoinkrementacji zadania nie przechodzi, to zakomentuj ten test. Nie będzie nam potrzebny w tych warsztatach. +- Na końcu uruchom aplikację konsolową i przetestuj jej działanie. Jeżeli w aplikacji zauważysz jakieś błędy w działaniu, to napraw je. + +**Jeżeli refaktor przebiegł pomyślnie, przejdź do dalszej części.** + +--- + +### **2. Utworzenie typu `User`** + +Reprezentuje pojedynczego użytkownika. +Utwórz klasę `User` w folderze `BusinessLogic`. + +**Cechy:** +- `Id`: Unikalny identyfikator użytkownika w formie `int`. +- `Name`: Nazwa użytkownika (wartość wymagana). +- `Zadania`: Lista przypisanych zadań. + +**Akcje:** +- Domyślny konstruktor bez parametrów: Utwórz **prywatny** konstruktor bez parametrów. Jest to obejście niezbędne do prawidłowego mapowania danych w ORM. +- Konstruktor z parametrami: Tworzy obiekt użytkownika na podstawie dostarczonego identyfikatora oraz nazwy. Lista zadań ma być utworzona i pusta. +- `ToString`: Użyj własnej wersji metody do wyświetlania informacji o użytkowniku w formacie `ID. Nazwa`. + +--- + +### **3. Rozszerzenie `TaskItem`** + +Reprezentuje pojedyncze zadanie. + +**Dodatkowe cechy:** +- `CreatedBy`: Twórca zadania (`User`, wartość wymagana), tylko do odczytu (bez settera) z prywatnym polem o nazwie `_createdBy`. +- `AssignedTo`: Osoba przypisana do wykonania zadania (`User`, wartość opcjonalna, na początku równa `null`), tylko do odczytu (bez settera) z prywatnym polem o nazwie `_assignedTo`. + +Niestety, ale ORM Dapper nie radzi sobie z mapowaniem właściwości do odczytu (bez settera), które pobrano przy użyciu łaczenia tabel (JOIN), które mają własny typ danych (inny niż systemowe). Z tego powodu, aby zachować hermetyzację danych i "ukryć przed światem" modyfikacje wartości, musimy zastosować obejście w postaci wprowadzenia prywatnego pola, które będzie przechowywało właściwą informację, a właściwość będzie tylko wyświetlała tę wartość. O tym jak będziemy używać tego obejścia wyjaśnimy później. + +**Dodatkowe akcje:** +- Domyślny konstruktor bez parametrów: Utwórz **prywatny** konstruktor bez parametrów. Jest to obejście niezbędne do prawidłowego mapowania danych w ORM. +- Dostosuj konstruktor z parametrami: Tworzy obiekt zadania na podstawie dostarczonego identyfikatora, opisu zadania, twórcy zadania oraz opcjonalnej daty zakończenia zadania. +- Dostosuj metodę `ToString`: niech metoda zwraca dodatkowo informację o przypisanym użytkowniku do zadania (o ile taki istnieje). +- Usuń mechanizm autoinkrementacji z modelu zadania. Identyfikator będzie przekazywany w konstruktorze. Na razie mechanizm nadawania ID zadaniom przejmie `TaskManagerService` (o tym za chwilę), a docelowo baza danych (o tym później). +- `AssignTo(User? assignedTo)`: Przypisuje użytkownika do zadania. Jeżeli użytkownik jest `null` to ma "odpiąć użytkownika od zadania" i ustawić wartość `null`. Metoda zwraca `void`. + +--- + +### **4. Rozszerzenie `TaskManagerService`** + +Reprezentuje serwis przechowujący i zarządzający listą zadań. + +**Dodatkowe cechy:** +- `_id`: Prywatna statyczna zmienna typu `int` o początkowej wartości `0`. + +**Zmienione akcje:** +- `AddAsync(description, createdBy, dueDate)`: Dodaje nowe zadanie do listy zadań z podanym opisem, ID twórcy zadania i opcjonalną datą realizacji. Tworząc nowy obiekt zadania przekaż parametr użytkownika `User` (utwórz go podająć ID createdBy i wpisz dowolną nazwę, nie jest to istotne w tym momencie). Zwraca utworzone zadanie. Tworząc nowe zadanie metoda zwiększaj licznik `_id` preinkrementując go. + +--- + +### **5. Dostosowanie testów `TaskItemTests`** + +1. Utwórz w klasie testowej prywatnego użytkownika `User` z przykłdowymi danymi, np. +```csharp +private User _createdBy = new User(1, "Ja"); +``` +2. Uzupełnij w każdej metodzie testowej wywołanie konstruktora `TaskItem` o wartość `id` oraz `createdBy`. Jako wartość `id` podaj dowolnie wymyśloną wartość, np. `1`, `2`, itd. Natomiast jako twórcę zadania przekaż `_createdBy`. +3. Nie uruchamiaj jeszcze testów, należy jeszcze dostosować testy `TaskManagerServiceTests`. + +--- + +### **6. Dostosowanie testów `TaskManagerServiceTests`** + +1. Utwórz w klasie testowej prywatne ID użytkownika, np. +```csharp +private readonly int _createdBy = 1; +``` +2. Uzupełnij w każdej metodzie testowej wywołanie metody `AddAsync` o wartość `createdBy`. Użyj zmiennej `_createdBy`. +3. Nie uruchamiaj jeszcze testów, należy jeszcze dostosować aplikację konsolową `Program`. + +--- + +### **7. Dostosowanie aplikacji konsolowej `Program`** + +1. Utwórz w klasie programu prywatne, statyczne ID użytkownika, np. +```csharp +private static int _createdBy = 1; +``` +2. Uzupełnij wywołanie metody `TaskManagerService.AddAsync` o wartość `createdBy`. Użyj zmiennej `_createdBy`. +3. Przekompiluj solucję. Uruchom wszystkie testy oraz aplikację. Jeżeli testy przechodzą oraz aplikacja działa, przejdź dalej, jeżeli nie to napraw powstałe błędy i przejrzyj jeszcze raz instrukcję. + +--- + +### **8. Utworzenie bazy danych `TaskManager`** + +Baza danych do przechowywania informacji o zadaniach i użytkownikach. +Napisz skrypt SQL do utworzenia kompletnej bazy danych z tabelami i relacjami. + +**Utwórz bazę danych `TaskManager`:** +- Napisz skrypt do utworzenia bazy danych `TaskManager`. +- Uruchom skrypt na bazie danych MS SQL Server. +- Ustaw nową bazę danych jako domyślną do użycia w kolejnych zapytaniach. + +**Utwórz tabelę `Users`:** +- `Id`: klucz główny o typie `INT`, automatycznie numerowany `IDENTITY(1,1)`. +- `Name`: kolumna typu `NVARCHAR(MAX)`, wymagana. +- Uruchom skrypt na bazie danych MS SQL Server. + +**Utwórz tabelę `TaskItems`:** +- `Id`: klucz główny o typie `INT`, automatycznie numerowany `IDENTITY(1,1)`. +- `Description`: kolumna typu `NVARCHAR(MAX)`, wymagana. +- `CreationDate`: kolumna typu `DATETIME`, wymagana. +- `DueDate`: kolumna typu `DATETIME`, niewymagana. +- `StartDate`: kolumna typu `DATETIME`, niewymagana. +- `DoneDate`: kolumna typu `DATETIME`, niewymagana. +- `Status`: kolumna typu `INT`, wymagana. +- `CreatedById`: klucz obcy typu `INT` do tabeli `Users`, wymagany. +- `AssignedToId`: klucz obcy typu `INT` do tabeli `Users`, niewymagany. +- Uruchom skrypt na bazie danych MS SQL Server. + +**Dodaj predefiniowanych użytkowników:** +- Napisz skrypt i uruchom go, aby dodać predefiniowanych użytkowników: + - W pierwszej kolejności dodaj użytkownika z Twoim imieniem i nazwiskiem. + - Dodaj dwóch innych użytkowników, np. `Anna Pawlak`, `Jan Nowak`. +- Upewnij się, że masz co najmniej trzech użytkowników w bazie danych. + +--- + +### **9. Konfiguracja Dapper i połączenia z bazą danych** + +1. Zainstaluj w głównym projekcie `TaskManager` paczki NuGet: `Microsoft.Data.SqlClient` i `Dapper`. +2. W aplikacji konsolowej w klasie `Program` utwórz prywatną stałą `ConnectionString`. Pamiętaj, aby konfiguracja wskazywała na użycie nowej bazy danych `TaskManager`. +3. Dla testu i poprawności działania bazy danych utwórz w klasie `Program` prywatną statyczną metodę `TestDbAsync` i wywołaj ją na początku metody `Main`. Użyj kodu dostarczonego poniżej. +4. Jeżeli test przejdzie pozytywnie to usuń wywołanie metody. + +
+Pokaż kod TestDbAsync + +```csharp +private static async Task TestDbAsync() +{ + using (var connection = new SqlConnection(ConnectionString)) + { + var sql = @"SELECT CONCAT( +'Tabela TaskItems ' +, CASE WHEN OBJECT_ID('TaskItems', 'U') IS NOT NULL THEN 'istnieje' ELSE 'nieistnieje' END +, CHAR(13)+CHAR(10) +, CONCAT('Tabela Users ', CASE WHEN OBJECT_ID('Users', 'U') IS NOT NULL THEN 'istnieje' ELSE 'nieistnieje' END) +)"; + var result = await connection.QueryFirstAsync(sql); + return result; + } +} +``` +
+ +--- + +### **10. Utworzenie szablonu repozytorium do komunikacji z bazą danych** + +Repozytorium (*ang. repository*) to koncept/wzorzec projektowy dla klasy komunikującej się z bazą danych. + +Klasa zawiera wyłącznie metody, które pobierają lub modyfikują dane w bazie danych. +Jej zadaniem jest rozdzielenie komunikacji z bazą danych od logiki biznesowej. + +Do zarządzania logiką biznesową służą tzw. domeny (*ang. domain*) czyli modele biznesowe oraz serwisy (*ang. services*). Modele domenowe przechowują informacje biznesowe oraz zarządzają dostępem do informacji (są odpowiedzialne za hermetyzację). Serwisy łączą/integrują modele biznesowe z zewnętrznymi systemami, np. bazami danych, API, itd. + +Repozytorium powinno być klasą implementującą interfejs `IRepository`. Interfejs służy temu, aby posiadać wiele implementacji repozytorium. Projekt z aplikacją konsolową może używać bazy danych, natomiast projekt z testami niekoniecznie. Testy są uruchamiane często i nie powinny mieć dostępu do głównej bazy danych. Zazywczaj testy pomijają komunikację z bazą danych poprzez użycie obiektu typu **mock** lub posiadają własną bazę danych tworzoną na żądanie, np. przy użyciu Dockera lub przechowującą dane w pamięci. + +W warsztatach w projekcie testów wykorzystamy prostą implementację `IRepository` w formie mocka. Będzie ona dostarczona w formie gotowej klasy do przekopiowania. Nie chcemy, aby uruchamianie testów powodowało zmiany w naszej głównej bazie danych. + +Zwróć uwagę, że odkąd będziemy używać repozytorium to metody serwisu będą hermetyczne, tj. dane zawsze będą aktualne i pobierane z bazy danych. W przypadku modyfikacji danych będzie następujący przepływ: +- Serwis najpierw pobierze aktualne dane z bazy przy pomocy repozytorium, które zwróci w formie modelu biznesowego, np. `TaskItem`. +- Następnie będziemy dokonywali modyfikacji tego modelu przy pomocy metod w klasie `TaskItem`. +- Efekt zmian wyślemy do repozytorium, aby zapisać je w bazie danych. + +**UWAGA: Należy pamiętać, że Dapper jest mikro ORM-em, a więc jeżeli dokonamy zmian w modelu to zmiany będą widoczne w bazie danych dopiero kiedy wprost wywołamy metody modyfikacji w repozytorium.** + +--- + +#### **Utwórz interfejs `IRepository` w folderze `BusinessLogic` z akcjami:** +- `GetAllUsersAsync()`: do pobierania wszystkich użytkowników. +- `GetUserByIdAsync(int userId)`: do pobierania użytkownika i podanym ID. +- `CreateTaskItemAsync(TaskItem newTaskItem)`: do tworzenia zadania, metoda ma zwracać ID utworzonego zadania. +- `UpdateTaskItemAsync(TaskItem newTaskItem)`: do aktualizacji zadania, zwraca informację czy aktualizacja powiodła się. +- `DeleteTaskItemAsync(int taskItemId)`: do usuwania zadania o podanym ID, zwraca informację czy usuwanie powiodło się. +- `GetTaskItemByIdAsync(int taskItemId)`: pobiera zadanie o podanym ID. +- `GetAllTaskItemsAsync()`: pobiera wszystkie zadania. +- `GetTaskItemsByStatusAsync(TaskItemStatus status)`: pobiera wszyskie zadania o podanym statusie. +- `GetTaskItemsByDescriptionAsync(string description)`: pobiera wszystkie zadania, w których występuje podana fraza w opisie. + +
+Pokaż kod źródłowy interfejsu IRepository + +```csharp +public interface IRepository +{ + Task GetAllUsersAsync(); + Task GetUserByIdAsync(int userId); + Task CreateTaskItemAsync(TaskItem newTaskItem); + Task UpdateTaskItemAsync(TaskItem newTaskItem); + Task DeleteTaskItemAsync(int taskItemId); + Task GetTaskItemByIdAsync(int taskItemId); + Task GetAllTaskItemsAsync(); + Task GetTaskItemsByStatusAsync(TaskItemStatus status); + Task GetTaskItemsByDescriptionAsync(string description); +} +``` +
+ +--- + +#### **Utwórz klasę `Repository` w folderze `BusinessLogic`:** +- Zaimplementuj interfejs `IRepository` domyślnym zachowaniem, tak aby wywołanie każdej metody wyrzucało wyjątek. W tym celu w IDE wywołaj polecenie `Implement missing members` lub użyj gotowego kodu dostarczonego poniżej. +- Dodaj konstruktor przyjmujący parametr `connectionString` i zachowaj jego wartość w zmiennej tylko do odczytu `_connectionString`. Użyjemy tego później. + +
+Pokaż domyślną implementację Repository + +```csharp +public class Repository : IRepository +{ + private readonly string _connectionString; + + public Repository(string connectionString) + { + _connectionString = connectionString; + } + + public Task GetAllUsersAsync() + { + throw new NotImplementedException(); + } + + public Task GetUserByIdAsync(int userId) + { + throw new NotImplementedException(); + } + + public Task CreateTaskItemAsync(TaskItem newTaskItem) + { + throw new NotImplementedException(); + } + + public Task UpdateTaskItemAsync(TaskItem newTaskItem) + { + throw new NotImplementedException(); + } + + public Task DeleteTaskItemAsync(int taskItemId) + { + throw new NotImplementedException(); + } + + public Task GetTaskItemByIdAsync(int taskItemId) + { + throw new NotImplementedException(); + } + + public Task GetAllTaskItemsAsync() + { + throw new NotImplementedException(); + } + + public Task GetTaskItemsByStatusAsync(TaskItemStatus status) + { + throw new NotImplementedException(); + } + + public Task GetTaskItemsByDescriptionAsync(string description) + { + throw new NotImplementedException(); + } +} +``` +
+ +--- + +#### **Użyj interfejsu `IRepository` w klasie `TaskManagerService`:** +- W klasie `TaskManagerService` utwórz prywatną zmienną tylko do odczytu `_repository` typu `IRepository`. +- Utwórz konstruktor `TaskManagerService` z parametrem `IRepository` i przekaż wartość do zmiennej `_repository`. Dzięki temu będziemy mogli używać serwisu zarówno w aplikacji konsolowej (w wersji z prawdziwą bazą danych) jak i w testach (wersja mock). +- Usuń dwie zmienne z klasy: z użyciem autoinkrementowanego ID dla zadań oraz listę zadań. Zastąpimy ich użyciem repozytorium. +- W metodzie `AddAsync` zastąp użcie autoinkrementowanego ID wartością `0`. +- We wszystkich metodach serwisu zastąp użycie metod na liście `_tasks`, odpowiednią metodą z repozytorium. Pamiętaj o użyciu `async/await`: + - `AddAsync`: na początku pobierz użytkownika metodą `_repository.GetUserByIdAsync` i wstaw go . Następnie zastąp użycie `_tasks.Add` wywołaniem `_repository.CreateTaskItemAsync`. Rezultat wywołania metody z repozytorium wykorzystaj do pobrania pełnego obiektu z bazy danych i zwróć jej wynik jako rezultat `AddAsync`. Do pobrania pełnego obiektu możesz użyć metody `GetAsync`. Docelowo metoda repozytorium zwróci użytkownika z uzupełnionym ID. + - `RemoveAsync`: zastąp użycie `_tasks.Remove` wywołaniem `_repository.DeleteTaskItemAsync` z parametrem ID zadania. + - `GetAsync`: zastąp użycie `_tasks.Find` wywołaniem `_repository.GetTaskItemByIdAsync`. + - `GetAllAsync`: zastąp użycie `_tasks.ToArray()` wywołaniem `_repository.GetAllTaskItemsAsync()`. + - `GetAllAsync(TaskItemStatus)`: zastąp użycie `_tasks.FindAll` wywołaniem `_repository.GetTaskItemsByStatusAsync`. + - `GetAllAsync(string)`: zastąp użycie `_tasks.FindAll` wywołaniem `_repository.GetTaskItemsByDescriptionAsync`. + - `ChangeStatusAsync`: dostosuj wywołanie metody, tak aby wynik wywołanie metod modelu `TaskItem` (np. `Open`, `Start`, `Done`) przechować w zmiennej pomocniczej. Następnie jeżeli udało się zmienić status, to należy zapisać zmiany w bazie danych poprzez wywołanie `_repository.UpdateTaskItemAsync` i przekazując aktualną wersję obiektu `TaskItem`. Metoda `ChangeStatusAsync` powinna zwracać rezultat wywołania metody z repozytorium, a jeżeli nie było to możliwe to ma zwrócić `false`. + +
+Pokaż kod źródłowy TaskManagerService + +```csharp +public class TaskManagerService +{ + private readonly IRepository _repository; + + public TaskManagerService(IRepository repository) + { + _repository = repository; + } + + public async Task AddAsync(string description, int createdBy, DateTime? dueDate) + { + var user = await _repository.GetUserByIdAsync(createdBy); + var task = new TaskItem(0, description, user, dueDate); + var id = await _repository.CreateTaskItemAsync(task); + return await GetAsync(id); + } + + public async Task RemoveAsync(int taskId) + { + var task = await GetAsync(taskId); + if (task != null) + return await _repository.DeleteTaskItemAsync(task.Id); + return false; + } + + public async Task GetAsync(int taskId) + { + return await _repository.GetTaskItemByIdAsync(taskId); + } + + public async Task GetAllAsync() + { + return await _repository.GetAllTaskItemsAsync(); + } + + public async Task GetAllAsync(TaskItemStatus itemStatus) + { + return await _repository.GetTaskItemsByStatusAsync(itemStatus); + } + + public async Task GetAllAsync(string description) + { + return await _repository.GetTaskItemsByDescriptionAsync(description); + } + + public async Task ChangeStatusAsync(int taskId, TaskItemStatus newStatus) + { + var task = await GetAsync(taskId); + if (task == null || task?.Status == newStatus) + return false; + + var result = ChangeStatus(task, newStatus); + if (result) + { + return await _repository.UpdateTaskItemAsync(task); + } + + return false; + } + + private bool ChangeStatus(TaskItem task, TaskItemStatus newStatus) + { + switch (newStatus) + { + case TaskItemStatus.ToDo: + return task.Open(); + case TaskItemStatus.InProgress: + return task.Start(); + case TaskItemStatus.Done: + return task.Done(); + default: + return false; + } + } +} +``` +
+ +--- + +#### **W klasie `Program` dostosuj tworzenie obiektu `TaskManagerService`:** +- Poszukaj linii kodu z tworzeniem obiektu `TaskManagerService` i przekaż w jego konstruktorze obiekt repozytorium z konfiguracją połączenia z bazą danych `new Repository(ConnectionString)`. Użyjemy tego później, ale na ten moment potrzebujemy działającego kodu. + +--- + +#### **W projekcie testów utwórz klasę `MockRepository`:** +- Zaimplementuj interfejs `IRepository` udający połączenie z bazą danych. W tym celu wykorzystaj dostarczony kod poniżej. +- `MockRepository` robi to co w pierwotnej wersji robił serwis, czyli przechowuje w pamięci listę zadań i umożliwia zarządzanie nimi na potrzeby testów. + +
+Pokaż implementację MockRepository + +```csharp +public class MockRepository : IRepository +{ + private int _taskId = 0; + private List _tasks = new List(); + + private List _users = new List { new User(1, "Ja") }; + + public async Task GetAllUsersAsync() => _users.ToArray(); + + public async Task GetUserByIdAsync(int userId) => _users.FirstOrDefault(u => u.Id == userId); + + public async Task CreateTaskItemAsync(TaskItem newTaskItem) + { + var newTask = new TaskItem(newTaskItem.Id == 0 ? ++_taskId : newTaskItem.Id, newTaskItem.Description, newTaskItem.CreatedBy, newTaskItem.DueDate); + _tasks.Add(newTask); + return newTask.Id; + } + + public async Task UpdateTaskItemAsync(TaskItem newTaskItem) + { + var result = await DeleteTaskItemAsync(newTaskItem.Id); + if (result) + _tasks.Add(newTaskItem); + return result; + } + + public async Task DeleteTaskItemAsync(int taskItemId) + { + var task = await GetTaskItemByIdAsync(taskItemId); + return _tasks.Remove(task); + } + + public async Task GetTaskItemByIdAsync(int taskItemId) => _tasks.Find(t => t.Id == taskItemId); + + public async Task GetAllTaskItemsAsync() => _tasks.ToArray(); + + public async Task GetTaskItemsByStatusAsync(TaskItemStatus status) => _tasks.Where(t => t.Status == status).ToArray(); + + public async Task GetTaskItemsByDescriptionAsync(string description) => + _tasks.FindAll(t => t.Description.Contains(description, StringComparison.InvariantCultureIgnoreCase)).ToArray(); +} +``` +
+ +--- + +#### **Użyj `MockRepository` w projekcie testów:** +- Przejdź do klasy `TaskManagerServiceTests` i dostosuj tworzenie obiektu `TaskManagerService` poprzez dodanie w konstruktorze `new MockRepository()`. + +--- + +#### **Weryfikacja:** +- Po wstępnej rozbudowie i małym refaktorze należy zweryfikować poprawność działania aplikacji. +- W pierwszej kolejności przebuduj solucję, aby sprawdzić, czy nie ma błędów kompilacji. Jeżeli jakieś błędy wystąpią, zbadaj je i rozwiąż. Pamiętaj, że możesz sprawdzić w internecie co mogą oznaczać błędy, a także możesz zapytać na Slacku. +- Następnie uruchom wszystkie testy jednostkowe `TaskItemTests` i `TaskManagerServiceTests`. Wszystkie testy powinny przechodzić (mieć zielony kolor). Jeżeli testy dają błędny wynik, to sprawdź dlaczego i napraw testy. Jeżeli metoda testowa do sprawdzania autoinkrementacji zadania nie przechodzi, to zakomentuj ten test. Nie będzie nam potrzebny w tych warsztatach. +- Nie testuj działania aplikacj, ponieważ użyliśmy w niej repozytorium, w którym wszystkie metody zgłaszają wyjątek `throw new NotImplementedException();`. + +--- + +### **11. Utworzenie metod repozytorium `Repository` ze skryptami SQL** + +Kiedy mamy utworzony szablon klasy repozytorium, możemy przystąpić do implementacji poszczególnych metod wykorzystując Dapper i MS SQL. + +W tej sekcji będziemy implementować metody w klasie `Repository`. Pamiętaj, aby w każdej metodzie najpierw nawiązać nowe połączenie z bazą danych. + +**UWAGA:** w tym miejscu będziemy musieli zastosować wcześniej wspomniane obejście, aby prawidłowo mapować dane zadania związane z twórcą zadania (`CreatedBy`) i osobą do niego przypisaną (`AssignedTo`). Wykorzystamy do tego poniższy kod klasy `DapperExtensions`. Dodaj go do folderu `BusinessLogic` i użyj metodę rozszerzającą `FixDapperMappings` na obiekcie klasy `TaskItem` we wskazanych metodach. + +
+Pokaż kod DapperExtensions + +Niniejsze rozwiązanie używa tzw. refleksji. Refleksja w C# to zdolność programu do analizy własnej struktury, informacji o typach i manipulowania nimi w trakcie działania programu. Pozwala na dynamiczne badanie, dostęp i modyfikację typów, metod, właściwości, pól itp. w czasie wykonania. Refleksja jest zaawansowanym i potężnym narzędziem, ale należy z nią obchodzić się ostrożnie, ponieważ może prowadzić do kodu trudnego do zrozumienia i utrzymania. Nie będziemy omawiali na tym kursie szczegółowego działania refleksji. + +```csharp +using System.Reflection; + +namespace TaskManager.BusinessLogic +{ + public static class DapperExtensions + { + public static TaskItem FixDapperMapping(this TaskItem taskItem, User createdBy, User assignedTo) + { + SetValueToObject(taskItem, "_createdBy", createdBy); + SetValueToObject(taskItem, "_assignedTo", assignedTo); + return taskItem; + } + + private static void SetValueToObject(object obj, string fieldName, object value) + { + var type = obj.GetType(); + var field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + field.SetValue(obj, value); + } + } +} +``` +
+ +#### **Wersja podstawowa:** + +- `GetAllUsersAsync`: Użyj odpowiedniej metody Dappera do pobrania wszystkich użytkowników (bez zadań). +- `GetUserByIdAsync`: Użyj odpowiedniej metody Dappera do pobrania jednego użytkownika (bez zadań). +- `CreateTaskItemAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu, który utworzy zadanie (przekazując opis, użytkownika tworzącego, datę utworzenia, satus, i datę ważności zadania) i zwróci na końcu ID nowego zadania (nadane przez bazę danych). Następnie pobierz na podstawie tego ID obiekt z bazy danych i zwróć go (możesz wykorzystać do tego metodę `GetTaskItemByIdAsync`). Do pobrania nadanego ID możesz użyć skryptu `SELECT SCOPE_IDENTITY();` +- `UpdateTaskItemAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu modyfikacji zadania (przekazując status, datę startu, datę zakończenia zadania, osobę przypisaną do zadania) na podstawie podanego ID. Zwróć informację z metody o tym, czy udało się zaktualizować zadanie. +- `DeleteTaskItemAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu, który usunie jedno zadanie na podstawie podanego ID. Zwróć informację z metody o tym, czy udało się usunąć zadanie. +- `GetTaskItemByIdAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu, który pobierze pełny obiekt zadania (tzn. wraz z przypisanymi do niego użytkownikami jako twórcy i wykonawcy zadania). Zwróć obiekt zadania. W tym miejscu musimy wykorzystać nasze obejście `FixDapperMapping`. Sprawdź podpowiedź znajdującą się poniżej. +- `GetAllTaskItemsAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu, który pobierze wszystkie pełne obiekty zadania (tzn. wraz z przypisanymi do niego użytkownikami jako twórcy i wykonawcy zadania). Zwróć tablicę zadań. W tym miejscu musimy wykorzystać nasze obejście `FixDapperMapping`. Sprawdź podpowiedź znajdującą się poniżej. +- `GetTaskItemsByStatusAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu, który pobierze wszystkie pełne obiekty zadania (tzn. wraz z przypisanymi do niego użytkownikami jako twórcy i wykonawcy zadania) na podstawie podanego statusu zadania. Zwróć tablicę zadań. W tym miejscu musimy wykorzystać nasze obejście `FixDapperMapping`. Sprawdź podpowiedź znajdującą się poniżej. +- `GetTaskItemsByDescriptionAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu, który pobierze wszystkie pełne obiekty zadania (tzn. wraz z przypisanymi do niego użytkownikami jako twórcy i wykonawcy zadania) na podstawie podanej frazy występującej w opisie zadania (użyj `%FRAZA%`). Zwróć tablicę zadań. W tym miejscu musimy wykorzystać nasze obejście `FixDapperMapping`. Sprawdź podpowiedź znajdującą się poniżej. + +
+Podpowiedź do użycia FixDapperMapping + +Przykład pobrania jednego elementu, ale przy użyciu `QueryAsync` (w przypadku pobierania relacji JOIN dla pojedynczego rekordu, musimy użyć `QueryAsync`): +```csharp +var tasks = await connection.QueryAsync( + sql, + (task, createdBy, assignedTo) => + { + return task.FixDapperMapping(createdBy, assignedTo); + }, + new {TaskId = taskItemId}); +return tasks.FirstOrDefault(); +``` + +Przykład pobierania wielu elementów bez filtrowania: +```csharp +var tasks = await connection.QueryAsync( + sql, + (task, createdBy, assignedTo) => + { + return task.FixDapperMapping(createdBy, assignedTo); + }); +return tasks.ToArray(); +``` + +Przykład pobrania wielu elementów z filtrowaniem: +```csharp +var tasks = await connection.QueryAsync( + sql, + (task, createdBy, assignedTo) => + { + return task.FixDapperMapping(createdBy, assignedTo); + }, + new {TaskStatus = status}); +return tasks.ToArray(); +``` +
+ +#### **Wersja rozszerzona (dla chętnych):** +- `GetAllUsersAsync`: Zmodyfikuj metodę tak, aby pobrała pełne obiekty użytkownika wraz z przypisanymi do niego zadaniami. +- `GetUserByIdAsync`: Zmodyfikuj metodę tak, aby pobrała pełny obiekt użytkownika wraz z przypisanymi do niego zadaniami na podstawie podanego ID użytkownika. + +--- + +### **12. Dodaj nowe funkcjonalności do aplikacji konsolowej.** + +Rozszerz opcje aplikacji konsolowej o: +- wyświetlanie użytkowników, +- przypisywanie zadania do użytkownika. + +Pamiętaj, aby wyświetlić nowe opcje w menu w interfejsie użytkownika. + +#### **Rozszerz wyświetlanie szczegółów o zadaniu:** +- Wyświetl dodatkowe informacje dla szczegółów zadania o twórcę zadania i osobę przypisaną do zadania. + +#### **Wyświetlanie listy użytkowników:** +- Rozszerz klasę `TaskManagerService` o metodę `GetAllUsersAsync`. Wykorzystaj istniejącą metodę repozytorium `GetAllUsersAsync`. +- W konsoli wywołaj nową metodę serwisu i wyświetl zwrócone dane. + +#### **Przypisywanie użytkownika do zadania:** +- Rozszerz klasę `TaskManagerService` o metodę `AssignToAsync(taskId, userId)`, która zwróci informację czy udało się ustawić wykonawcę zadania. Niech metoda pobierze aktualną wersję zadania oraz użytkownika, następnie w modelu biznesowym zadania wywołaj metodę `AssignTo`, a na koniec zaktualizuj model zadania w bazie danych. ID użytkownika jest opcjonalne i może przyjąć wartość `null`, wówczas należy odsunąć użytkownika od zadania (i przekazać `null` do metody `AssignTo`). Metoda `AssignToAsync` powinna używać już istniejące metody serwisu i repozytorium, aby nie powielać kodu. +- W konsoli wywołaj nową metodę serwisu i wyświetl stosowny komunikat. + +--- + +### **13. Dodatkowe testy jednostkowe.** + +Dopisz testy jednostkowe sprawdzające `TaskItem` oraz `TaskManagerService`. + +#### **`TaskItemTests`:** +- Dopisz dwa scenariusze testowe sprawdzający działanie metody `AssignTo`, w przypadku gdy: + - podamy obiekt użytkownika, + - podamy wartość `null`. + +#### **`TaskManagerServiceTests`:** +- Dopisz cztery scenariusze testowe sprawdzające działanie metody `AssignToAsync`, w przypadku gdy: + - podamy właściwe ID zadania i użytkownika (przypisze zadanie do użytkownika), + - podamy właściwe ID zadania i pustego użytkownika (ustawi `null` przypisanemu użytkownikowi), + - podamy właściwe ID zadania, ale nie właściwe ID użytkownika (nie ustawi użytkownika), + - podamy niewłaściwe ID zadania (nie ustawi użytkownika). + +--- + +### **14. GRATULACJE! Właśnie jesteś na końcu warsztatów.** + +Gratulujemy i dziękujemy za aktywny udział w warsztatach. Twoje zaangażowanie i chęć do nauki są dla nas inspiracją. Życzymy powodzenia w dalszej części kursu! + +--- + +### **Aplikację możesz dowolnie rozszerzyć o dodatkowe funkcjonalności.** + +Pamiętaj, że ogranicza Cię tylko Twoja wyobraźnia. Możesz dalej rozwijać tę aplikację i rozszerzać ją o dodatkowe funkcjonalności, np.: +- wyświetlanie Twoich zadań, +- wyświetlanie zadań pogrupowanych po użytkownikach wykonujących je, +- wyświetlanie nieprzypisanych zadań, +- i wiele, wiele innych... + + +--- +--- +--- +--- + +## Podsumowanie warsztatów TaskManager z Dapper + +Po intensywnej pracy pełnej nauki i programowania, nadszedł czas na podsumowanie warsztatów dotyczących tworzenia aplikacji TaskManager w języku C#. + +### Główne punkty warsztatu + +1. **Podział aplikacji na warstwy**: Podzieliliśmy kod aplikacji na mniejsze bloczki: +- modele biznesowe (warstwa domeny), +- repozytorium (warstwa dostępu do danych), +- serwis (warstwa aplikacji), +- konsola (warstwa prezentacji). + +2. **Rozbudowanie modeli**: Rozbudowaliśmy model zadania o nowe cechy i akcje. Utworzliśmy nowy model użytkownika. + +3. **Utworzenie bazy danych**: Utworzyliśmy bazę danych `TaskManager` z tabelami zadań i użytkowników. + +4. **Połączenie z bazą danych przy użyciu Dapper**: Zainstalowaliśmy i skonfigurowaliśmy paczkę NuGet Dapper. Utorzyliśmy interfejs i repozytorium, za pomocą którego łączymy się z bazą danych. + +5. **Rozbudowanie testów**: Zmodyfikowaliśmy testy o wykorzystanie mock-a repozytorium. Dopisaliśmy nowe scenariusze testowe. + +6. **Rozbudowanie aplikacji konsolowej**: Zmodyfikowaliśmy aplikację konsolową i wprowadziliśmy programowanie asynchroniczne. Dodaliśmy nowe funkcje do aplikacji. + +### Dalsze kroki + +Zachęcamy do rozwijania aplikacji TaskManager. Możliwe są takie rozszerzenia jak: dodawanie priorytetów dla zadań, dodanie śledzenia historii zmian w zadaniu, dodawanie komentarzy do zadań, tworzenie interfejsu programistycznego WebAPI i interfejsu graficznego dla użytkownika w formie strony WWW. + +### Gratulacje! + +Życzymy powodzenia w dalszej części kursu! diff --git a/TaskManager.Tests/MockRepository.cs b/TaskManager.Tests/MockRepository.cs new file mode 100644 index 0000000..60369a4 --- /dev/null +++ b/TaskManager.Tests/MockRepository.cs @@ -0,0 +1,46 @@ +using TaskManager.BusinessLogic; + +namespace TaskManager.Tests +{ + public class MockRepository : IRepository + { + private int _taskId = 0; + private List _tasks = new List(); + + private List _users = new List { new User(1, "Ja") }; + + public async Task GetAllUsersAsync() => _users.ToArray(); + + public async Task GetUserByIdAsync(int userId) => _users.FirstOrDefault(u => u.Id == userId); + + public async Task CreateTaskItemAsync(TaskItem newTaskItem) + { + var newTask = new TaskItem(newTaskItem.Id == 0 ? ++_taskId : newTaskItem.Id, newTaskItem.Description, newTaskItem.CreatedBy, newTaskItem.DueDate); + _tasks.Add(newTask); + return newTask.Id; + } + + public async Task UpdateTaskItemAsync(TaskItem newTaskItem) + { + var result = await DeleteTaskItemAsync(newTaskItem.Id); + if (result) + _tasks.Add(newTaskItem); + return result; + } + + public async Task DeleteTaskItemAsync(int taskItemId) + { + var task = await GetTaskItemByIdAsync(taskItemId); + return _tasks.Remove(task); + } + + public async Task GetTaskItemByIdAsync(int taskItemId) => _tasks.Find(t => t.Id == taskItemId); + + public async Task GetAllTaskItemsAsync() => _tasks.ToArray(); + + public async Task GetTaskItemsByStatusAsync(TaskItemStatus status) => _tasks.Where(t => t.Status == status).ToArray(); + + public async Task GetTaskItemsByDescriptionAsync(string description) => + _tasks.FindAll(t => t.Description.Contains(description, StringComparison.InvariantCultureIgnoreCase)).ToArray(); + } +} \ No newline at end of file diff --git a/TaskManager.Tests/TaskTests.cs b/TaskManager.Tests/TaskItemTests.cs similarity index 56% rename from TaskManager.Tests/TaskTests.cs rename to TaskManager.Tests/TaskItemTests.cs index 7838526..538fab4 100644 --- a/TaskManager.Tests/TaskTests.cs +++ b/TaskManager.Tests/TaskItemTests.cs @@ -1,26 +1,25 @@ -//Task i TaskStatus już istnieją w przestrzeni System.Threading.Tasks, która jest automatycznie importowana. -//Musimy rozwiązać konflikt nazw stosując aliasy. -using Task = TaskManager.BusinessLogic.Task; -using TaskStatus = TaskManager.BusinessLogic.TaskStatus; +using TaskManager.BusinessLogic; namespace TaskManager.Tests { - public class TaskTests + public class TaskItemTests { + private User _createdBy = new User(1, "Ja"); + [Fact] public void Should_CreateTask_WithAutoIncrementedId() { - var task1 = new Task("Test task 1", null); - var task2 = new Task("Test task 2", null); + var task1 = new TaskItem(1, "Test task 1", _createdBy, null); + var task2 = new TaskItem(2, "Test task 2", _createdBy, null); Assert.True(task1.Id > 0); - Assert.Equal(task1.Id + 1, task2.Id); + Assert.True(task1.Id < task2.Id); } [Fact] public void Should_SetCreationDate_WhenCreatingTask() { - var task = new Task("Test task", null); + var task = new TaskItem(1, "Test task", _createdBy, null); var difference = DateTime.Now - task.CreationDate; Assert.True(difference.TotalSeconds < 1); @@ -30,7 +29,7 @@ public void Should_SetCreationDate_WhenCreatingTask() public void Should_SetDueDate_WhenProvided() { var dueDate = DateTime.Now.AddDays(7); - var task = new Task("Test task", dueDate); + var task = new TaskItem(1, "Test task", _createdBy, dueDate); Assert.Equal(dueDate, task.DueDate); } @@ -38,26 +37,26 @@ public void Should_SetDueDate_WhenProvided() [Fact] public void Should_SetStatusToTodo_WhenTaskIsCreated() { - var task = new Task("Test task", null); + var task = new TaskItem(1, "Test task", _createdBy, null); - Assert.Equal(TaskStatus.ToDo, task.Status); + Assert.Equal(TaskItemStatus.ToDo, task.Status); } [Fact] public void Should_ChangeStatus_ToInProgress_WhenStartIsCalled() { - var task = new Task("Test task", null); + var task = new TaskItem(1, "Test task", _createdBy, null); bool result = task.Start(); Assert.True(result); - Assert.Equal(TaskStatus.InProgress, task.Status); + Assert.Equal(TaskItemStatus.InProgress, task.Status); } [Fact] public void Should_SetStartDate_WhenStartIsCalled() { - var task = new Task("Test task", null); + var task = new TaskItem(1, "Test task", _createdBy, null); task.Start(); @@ -69,31 +68,31 @@ public void Should_SetStartDate_WhenStartIsCalled() [Fact] public void Should_NotChangeStatus_ToInProgress_IfAlreadyInProgress() { - var task = new Task("Test task", null); + var task = new TaskItem(1, "Test task", _createdBy, null); task.Start(); bool result = task.Start(); Assert.False(result); - Assert.Equal(TaskStatus.InProgress, task.Status); + Assert.Equal(TaskItemStatus.InProgress, task.Status); } [Fact] public void Should_ChangeStatus_ToDone_WhenDoneIsCalledAndStatusIsInProgress() { - var task = new Task("Test task", null); + var task = new TaskItem(1, "Test task", _createdBy, null); task.Start(); bool result = task.Done(); Assert.True(result); - Assert.Equal(TaskStatus.Done, task.Status); + Assert.Equal(TaskItemStatus.Done, task.Status); } [Fact] public void Should_SetDoneDate_WhenDoneIsCalled() { - var task = new Task("Test task", null); + var task = new TaskItem(1, "Test task", _createdBy, null); task.Start(); task.Done(); @@ -106,18 +105,18 @@ public void Should_SetDoneDate_WhenDoneIsCalled() [Fact] public void Should_NotChangeStatus_ToDone_IfStatusIsNotInProgress() { - var task = new Task("Test task", null); + var task = new TaskItem(1, "Test task", _createdBy, null); bool result = task.Done(); Assert.False(result); - Assert.Equal(TaskStatus.ToDo, task.Status); + Assert.Equal(TaskItemStatus.ToDo, task.Status); } [Fact] public void Should_CalculateDuration_WhenStatusIsInProgress() { - var task = new Task("Test task", null); + var task = new TaskItem(1, "Test task", _createdBy, null); task.Start(); var duration = task.Duration; @@ -128,11 +127,32 @@ public void Should_CalculateDuration_WhenStatusIsInProgress() [Fact] public void Should_ReturnNullDuration_WhenStatusIsTodo() { - var task = new Task("Test task", null); + var task = new TaskItem(1, "Test task", _createdBy, null); var duration = task.Duration; Assert.Null(duration); } + + [Fact] + public void Should_AssignTo_User() + { + var task = new TaskItem(1, "Test task", _createdBy, null); + + task.AssignTo(_createdBy); + + Assert.Equal(_createdBy, task.AssignedTo); + } + + [Fact] + public void Should_Unassign_User_When_Null_Passed() + { + var task = new TaskItem(1, "Test task", _createdBy, null); + task.AssignTo(_createdBy); + + task.AssignTo(null); + + Assert.Null(task.AssignedTo); + } } } \ No newline at end of file diff --git a/TaskManager.Tests/TaskManagerServiceTests.cs b/TaskManager.Tests/TaskManagerServiceTests.cs index 0d6824c..3525db0 100644 --- a/TaskManager.Tests/TaskManagerServiceTests.cs +++ b/TaskManager.Tests/TaskManagerServiceTests.cs @@ -1,131 +1,182 @@ using TaskManager.BusinessLogic; -//TaskStatus już istnieje w przestrzeni System.Threading.Tasks, która jest automatycznie importowana. -//Musimy rozwiązać konflikt nazw stosując alias. -using TaskStatus = TaskManager.BusinessLogic.TaskStatus; namespace TaskManager.Tests { public class TaskManagerServiceTests { + private readonly int _createdBy = 1; + [Fact] - public void Should_AddTask_ToTaskList() + public async Task Should_AddTask_ToTaskList() { - var service = new TaskManagerService(); + var service = new TaskManagerService(new MockRepository()); - var task = service.Add("Test task", DateTime.Now.AddDays(5)); + var task = await service.AddAsync("Test task", _createdBy, DateTime.Now.AddDays(5)); Assert.NotNull(task); - Assert.Single(service.GetAll()); + Assert.Single(await service.GetAllAsync()); } [Fact] - public void Should_RemoveTask_ByTaskId() + public async Task Should_RemoveTask_ByTaskId() { - var service = new TaskManagerService(); - var task = service.Add("Test task", null); + var service = new TaskManagerService(new MockRepository()); + var task = await service.AddAsync("Test task", _createdBy, null); - bool result = service.Remove(task.Id); + bool result = await service.RemoveAsync(task.Id); Assert.True(result); - Assert.Empty(service.GetAll()); + Assert.Empty(await service.GetAllAsync()); } [Fact] - public void Should_NotRemoveTask_WhenTaskIdDoesNotExist() + public async Task Should_NotRemoveTask_WhenTaskIdDoesNotExist() { - var service = new TaskManagerService(); - service.Add("Test task", null); + var service = new TaskManagerService(new MockRepository()); + await service.AddAsync("Test task", _createdBy, null); - bool result = service.Remove(999); + bool result = await service.RemoveAsync(999); Assert.False(result); - Assert.Single(service.GetAll()); + Assert.Single(await service.GetAllAsync()); } [Fact] - public void Should_GetTask_ByTaskId() + public async Task Should_GetTask_ByTaskId() { - var service = new TaskManagerService(); - var task = service.Add("Test task", null); + var service = new TaskManagerService(new MockRepository()); + var task = await service.AddAsync("Test task", _createdBy, null); - var retrievedTask = service.Get(task.Id); + var retrievedTask = await service.GetAsync(task.Id); Assert.NotNull(retrievedTask); Assert.Equal(task.Id, retrievedTask.Id); } [Fact] - public void Should_GetAllTasks_WithNoFilter() + public async Task Should_GetAllTasks_WithNoFilter() { - var service = new TaskManagerService(); - service.Add("Test task 1", null); - service.Add("Test task 2", null); + var service = new TaskManagerService(new MockRepository()); + await service.AddAsync("Test task 1", _createdBy, null); + await service.AddAsync("Test task 2", _createdBy, null); - var tasks = service.GetAll(); + var tasks = await service.GetAllAsync(); Assert.Equal(2, tasks.Length); } [Fact] - public void Should_GetTasks_ByStatus() + public async Task Should_GetTasks_ByStatus() { - var service = new TaskManagerService(); - var task1 = service.Add("Test task 1", null); + var service = new TaskManagerService(new MockRepository()); + var task1 = await service.AddAsync("Test task 1", _createdBy, null); task1.Start(); - service.Add("Test task 2", null); + await service.AddAsync("Test task 2", _createdBy, null); - var inProgressTasks = service.GetAll(TaskStatus.InProgress); + var inProgressTasks = await service.GetAllAsync(TaskItemStatus.InProgress); Assert.Single(inProgressTasks); Assert.Equal(task1.Id, inProgressTasks.First().Id); } [Fact] - public void Should_GetTasks_ByDescription() + public async Task Should_GetTasks_ByDescription() { - var service = new TaskManagerService(); - service.Add("Unique test task", null); - service.Add("Test task 2", null); + var service = new TaskManagerService(new MockRepository()); + await service.AddAsync("Unique test task", _createdBy, null); + await service.AddAsync("Test task 2", _createdBy, null); - var tasks = service.GetAll("Unique"); + var tasks = await service.GetAllAsync("Unique"); Assert.Single(tasks); Assert.Equal("Unique test task", tasks.First().Description); } [Fact] - public void Should_ChangeTaskStatus_WhenValid() + public async Task Should_ChangeTaskStatus_WhenValid() + { + var service = new TaskManagerService(new MockRepository()); + var task = await service.AddAsync("Test task", _createdBy, null); + + bool result = await service.ChangeStatusAsync(task.Id, TaskItemStatus.InProgress); + + Assert.True(result); + Assert.Equal(TaskItemStatus.InProgress, task.Status); + } + + [Fact] + public async Task Should_NotChangeTaskStatus_WhenInvalidTransition() + { + var service = new TaskManagerService(new MockRepository()); + var task = await service.AddAsync("Test task", _createdBy, null); + + bool result = await service.ChangeStatusAsync(task.Id, TaskItemStatus.Done); + + Assert.False(result); + Assert.Equal(TaskItemStatus.ToDo, task.Status); + } + + [Fact] + public async Task Should_NotChangeTaskStatus_WhenTaskIdDoesNotExist() + { + var service = new TaskManagerService(new MockRepository()); + await service.AddAsync("Test task", _createdBy, null); + + bool result = await service.ChangeStatusAsync(999, TaskItemStatus.Done); + + Assert.False(result); + } + + [Fact] + public async void Should_Assign_ExistingUser_To_ExistingTask() + { + var service = new TaskManagerService(new MockRepository()); + await service.AddAsync("Test task", _createdBy, null); + + var result = await service.AssignToAsync(1, _createdBy); + + var task = await service.GetAsync(1); + Assert.True(result); + Assert.Equal(_createdBy, task.AssignedTo.Id); + } + + [Fact] + public async void Should_Unassign_User_From_Task() { - var service = new TaskManagerService(); - var task = service.Add("Test task", null); + var service = new TaskManagerService(new MockRepository()); + await service.AddAsync("Test task", _createdBy, null); + await service.AssignToAsync(1, _createdBy); - bool result = service.ChangeStatus(task.Id, TaskStatus.InProgress); + var result = await service.AssignToAsync(1, null); + var task = await service.GetAsync(1); Assert.True(result); - Assert.Equal(TaskStatus.InProgress, task.Status); + Assert.Null(task.AssignedTo); } [Fact] - public void Should_NotChangeTaskStatus_WhenInvalidTransition() + public async void Should_NotAssign_NotExistingUser_To_ExistingTask() { - var service = new TaskManagerService(); - var task = service.Add("Test task", null); + var service = new TaskManagerService(new MockRepository()); + await service.AddAsync("Test task", _createdBy, null); - bool result = service.ChangeStatus(task.Id, TaskStatus.Done); + var result = await service.AssignToAsync(1, 999); + var task = await service.GetAsync(1); Assert.False(result); - Assert.Equal(TaskStatus.ToDo, task.Status); + Assert.Null(task.AssignedTo); } [Fact] - public void Should_NotChangeTaskStatus_WhenTaskIdDoesNotExist() + public async void Should_NotAssign_ExistingUser_To_NotExistingTask() { - var service = new TaskManagerService(); - service.Add("Test task", null); + var service = new TaskManagerService(new MockRepository()); - bool result = service.ChangeStatus(999, TaskStatus.Done); + var result = await service.AssignToAsync(1, 1); + var task = await service.GetAsync(1); Assert.False(result); + Assert.Null(task); } } } diff --git a/TaskManager/BusinessLogic/DapperExtensions.cs b/TaskManager/BusinessLogic/DapperExtensions.cs new file mode 100644 index 0000000..cdf0499 --- /dev/null +++ b/TaskManager/BusinessLogic/DapperExtensions.cs @@ -0,0 +1,21 @@ +using System.Reflection; + +namespace TaskManager.BusinessLogic +{ + public static class DapperExtensions + { + public static TaskItem FixDapperMapping(this TaskItem taskItem, User createdBy, User assignedTo) + { + SetValueToObject(taskItem, "_createdBy", createdBy); + SetValueToObject(taskItem, "_assignedTo", assignedTo); + return taskItem; + } + + private static void SetValueToObject(object obj, string fieldName, object value) + { + var type = obj.GetType(); + var field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + field.SetValue(obj, value); + } + } +} \ No newline at end of file diff --git a/TaskManager/BusinessLogic/IRepository.cs b/TaskManager/BusinessLogic/IRepository.cs new file mode 100644 index 0000000..6cc9ef0 --- /dev/null +++ b/TaskManager/BusinessLogic/IRepository.cs @@ -0,0 +1,15 @@ +namespace TaskManager.BusinessLogic +{ + public interface IRepository + { + Task GetAllUsersAsync(); + Task GetUserByIdAsync(int userId); + Task CreateTaskItemAsync(TaskItem newTaskItem); + Task UpdateTaskItemAsync(TaskItem newTaskItem); + Task DeleteTaskItemAsync(int taskItemId); + Task GetTaskItemByIdAsync(int taskItemId); + Task GetAllTaskItemsAsync(); + Task GetTaskItemsByStatusAsync(TaskItemStatus status); + Task GetTaskItemsByDescriptionAsync(string description); + } +} \ No newline at end of file diff --git a/TaskManager/BusinessLogic/Repository.cs b/TaskManager/BusinessLogic/Repository.cs new file mode 100644 index 0000000..3eff97d --- /dev/null +++ b/TaskManager/BusinessLogic/Repository.cs @@ -0,0 +1,165 @@ +using System.Data.SqlClient; +using System.Reflection; +using Dapper; + +namespace TaskManager.BusinessLogic +{ + public class Repository : IRepository + { + private readonly string _connectionString; + + public Repository(string connectionString) + { + _connectionString = connectionString; + } + + public async Task GetAllUsersAsync() + { + using (var connection = new SqlConnection(_connectionString)) + { + var users = await connection.QueryAsync("SELECT * FROM Users"); + return users.ToArray(); + } + } + + public async Task GetUserByIdAsync(int userId) + { + using (var connection = new SqlConnection(_connectionString)) + { + var user = await connection.QueryFirstAsync("SELECT * FROM Users WHERE Id = @Id", new {Id = userId}); + return user; + } + } + + public async Task CreateTaskItemAsync(TaskItem newTaskItem) + { + using (var connection = new SqlConnection(_connectionString)) + { + var sql = + "INSERT INTO TaskItems (Description, CreatedById, CreationDate, Status, DueDate) " + + "VALUES (@Description, @CreatedById, @CreationDate, @Status, @DueDate);" + + "SELECT SCOPE_IDENTITY();"; + var newTask = new + { + Description = newTaskItem.Description, + CreatedById = newTaskItem.CreatedBy.Id, + CreationDate = newTaskItem.CreationDate, + Status = newTaskItem.Status, + DueDate = newTaskItem.DueDate + }; + var id = await connection.ExecuteScalarAsync(sql, newTask); + return id; + } + } + + public async Task UpdateTaskItemAsync(TaskItem newTaskItem) + { + using (var connection = new SqlConnection(_connectionString)) + { + var sql = + "UPDATE TaskItems " + + "SET Status = @Status, StartDate = @StartDate, DoneDate = @DoneDate, AssignedToId = @AssignedTo " + + "WHERE Id = @Id"; + + var updateTask = new + { + Id = newTaskItem.Id, + Status = newTaskItem.Status, + StartDate = newTaskItem.StartDate, + DoneDate = newTaskItem.DoneDate, + AssignedTo = newTaskItem.AssignedTo?.Id + }; + var result = await connection.ExecuteAsync(sql, updateTask); + return result == 1; + } + } + + public async Task DeleteTaskItemAsync(int taskItemId) + { + using (var connection = new SqlConnection(_connectionString)) + { + var sql = "DELETE FROM TaskItems WHERE Id = @Id"; + var result = await connection.ExecuteAsync(sql, new {Id = taskItemId}); + return result == 1; + } + } + + public async Task GetTaskItemByIdAsync(int taskItemId) + { + using (var connection = new SqlConnection(_connectionString)) + { + var sql = + "SELECT task.*, createdBy.*, assignedTo.* FROM TaskItems task " + + "INNER JOIN Users createdBy ON createdBy.Id = task.CreatedById " + + "LEFT JOIN Users assignedTo ON assignedTo.Id = task.AssignedToId " + + "WHERE task.Id = @TaskId"; + var tasks = await connection.QueryAsync( + sql, + (task, createdBy, assignedTo) => + { + return task.FixDapperMapping(createdBy, assignedTo); + }, + new {TaskId = taskItemId}); + return tasks.FirstOrDefault(); + } + } + + public async Task GetAllTaskItemsAsync() + { + using (var connection = new SqlConnection(_connectionString)) + { + var sql = + "SELECT task.*, createdBy.*, assignedTo.* FROM TaskItems task " + + "INNER JOIN Users createdBy ON createdBy.Id = task.CreatedById " + + "LEFT JOIN Users assignedTo ON assignedTo.Id = task.AssignedToId"; + var tasks = await connection.QueryAsync( + sql, + (task, createdBy, assignedTo) => + { + return task.FixDapperMapping(createdBy, assignedTo); + }); + return tasks.ToArray(); + } + } + + public async Task GetTaskItemsByStatusAsync(TaskItemStatus status) + { + using (var connection = new SqlConnection(_connectionString)) + { + var sql = + "SELECT task.*, createdBy.*, assignedTo.* FROM TaskItems task " + + "INNER JOIN Users createdBy ON createdBy.Id = task.CreatedById " + + "LEFT JOIN Users assignedTo ON assignedTo.Id = task.AssignedToId " + + "WHERE Task.Status = @TaskStatus"; + var tasks = await connection.QueryAsync( + sql, + (task, createdBy, assignedTo) => + { + return task.FixDapperMapping(createdBy, assignedTo); + }, + new {TaskStatus = status}); + return tasks.ToArray(); + } + } + + public async Task GetTaskItemsByDescriptionAsync(string description) + { + using (var connection = new SqlConnection(_connectionString)) + { + var sql = + "SELECT task.*, createdBy.*, assignedTo.* FROM TaskItems task " + + "INNER JOIN Users createdBy ON createdBy.Id = task.CreatedById " + + "LEFT JOIN Users assignedTo ON assignedTo.Id = task.AssignedToId " + + "WHERE Task.Description LIKE @Description"; + var tasks = await connection.QueryAsync( + sql, + (task, createdBy, assignedTo) => + { + return task.FixDapperMapping(createdBy, assignedTo); + }, + new {Description = $"%{description}%"}); + return tasks.ToArray(); + } + } + } +} \ No newline at end of file diff --git a/TaskManager/BusinessLogic/Task.cs b/TaskManager/BusinessLogic/TaskItem.cs similarity index 54% rename from TaskManager/BusinessLogic/Task.cs rename to TaskManager/BusinessLogic/TaskItem.cs index 9c90f88..43da001 100644 --- a/TaskManager/BusinessLogic/Task.cs +++ b/TaskManager/BusinessLogic/TaskItem.cs @@ -1,9 +1,9 @@ namespace TaskManager.BusinessLogic { - public class Task + public class TaskItem { - private static int _id; - + private User _createdBy; + private User? _assignedTo; public int Id { get; } public string Description { get; set; } @@ -18,22 +18,28 @@ public class Task public TimeSpan? Duration => StartDate != null ? (DoneDate ?? DateTime.Now) - StartDate.Value : null; - public TaskStatus Status { get; private set; } = TaskStatus.ToDo; + public TaskItemStatus Status { get; private set; } = TaskItemStatus.ToDo; + + public User CreatedBy => _createdBy; + public User? AssignedTo => _assignedTo; + + private TaskItem() { } - public Task(string description, DateTime? dueDate) + public TaskItem(int id, string description, User createdBy, DateTime? dueDate) { - Id = ++_id; + Id = id; Description = description; CreationDate = DateTime.Now; + _createdBy = createdBy; DueDate = dueDate; } public bool Start() { - if (Status == TaskStatus.InProgress) + if (Status == TaskItemStatus.InProgress) return false; - Status = TaskStatus.InProgress; + Status = TaskItemStatus.InProgress; StartDate = DateTime.Now; DoneDate = null; return true; @@ -41,10 +47,10 @@ public bool Start() public bool Open() { - if (Status == TaskStatus.ToDo) + if (Status == TaskItemStatus.ToDo) return false; - Status = TaskStatus.ToDo; + Status = TaskItemStatus.ToDo; StartDate = null; DoneDate = null; return true; @@ -52,17 +58,22 @@ public bool Open() public bool Done() { - if (Status != TaskStatus.InProgress) + if (Status != TaskItemStatus.InProgress) return false; - Status = TaskStatus.Done; + Status = TaskItemStatus.Done; DoneDate = DateTime.Now; return true; } + public void AssignTo(User? assignedTo) + { + _assignedTo = assignedTo; + } + public override string ToString() { - return $"{Id} - {Description} ({Status})"; + return $"{Id} - {Description} ({Status}) @{AssignedTo?.Name ?? "nieprzypisane"}"; } } } \ No newline at end of file diff --git a/TaskManager/BusinessLogic/TaskStatus.cs b/TaskManager/BusinessLogic/TaskItemStatus.cs similarity index 75% rename from TaskManager/BusinessLogic/TaskStatus.cs rename to TaskManager/BusinessLogic/TaskItemStatus.cs index ba6f741..2042169 100644 --- a/TaskManager/BusinessLogic/TaskStatus.cs +++ b/TaskManager/BusinessLogic/TaskItemStatus.cs @@ -1,6 +1,6 @@ namespace TaskManager.BusinessLogic { - public enum TaskStatus + public enum TaskItemStatus { ToDo, InProgress, diff --git a/TaskManager/BusinessLogic/TaskManagerService.cs b/TaskManager/BusinessLogic/TaskManagerService.cs index 4e213fc..738c991 100644 --- a/TaskManager/BusinessLogic/TaskManagerService.cs +++ b/TaskManager/BusinessLogic/TaskManagerService.cs @@ -2,63 +2,96 @@ namespace TaskManager.BusinessLogic { public class TaskManagerService { - private List _tasks = new List(); + private readonly IRepository _repository; - public Task Add(string description, DateTime? dueDate) + public TaskManagerService(IRepository repository) { - var task = new Task(description, dueDate); - _tasks.Add(task); - return task; + _repository = repository; } - public bool Remove(int taskId) + public async Task AddAsync(string description, int createdBy, DateTime? dueDate) { - var task = Get(taskId); + var user = await _repository.GetUserByIdAsync(createdBy); + var task = new TaskItem(0, description, user, dueDate); + var id = await _repository.CreateTaskItemAsync(task); + return await GetAsync(id); + } + + public async Task RemoveAsync(int taskId) + { + var task = await GetAsync(taskId); if (task != null) - return _tasks.Remove(task); + return await _repository.DeleteTaskItemAsync(task.Id); return false; } - public Task Get(int taskId) + public async Task GetAsync(int taskId) { - return _tasks.Find(t => t.Id == taskId); + return await _repository.GetTaskItemByIdAsync(taskId); } - public Task[] GetAll() + public async Task GetAllAsync() { - return _tasks.ToArray(); + return await _repository.GetAllTaskItemsAsync(); } - public Task[] GetAll(TaskStatus status) + public async Task GetAllAsync(TaskItemStatus itemStatus) { - return _tasks.FindAll(t => t.Status == status).ToArray(); + return await _repository.GetTaskItemsByStatusAsync(itemStatus); } - public Task[] GetAll(string description) + public async Task GetAllAsync(string description) { - // Przeciążona wersja Contains przyjmuje drugi parametr jako opcje porównania tekstu, - // gdzie możemy wskazać, aby przy porównaniu pomijać wielkość liter - return _tasks.FindAll(t => t.Description.Contains(description, StringComparison.InvariantCultureIgnoreCase)).ToArray(); + return await _repository.GetTaskItemsByDescriptionAsync(description); } - public bool ChangeStatus(int taskId, TaskStatus newStatus) + public async Task ChangeStatusAsync(int taskId, TaskItemStatus newStatus) { - var task = Get(taskId); + var task = await GetAsync(taskId); if (task == null || task?.Status == newStatus) return false; + var result = ChangeStatus(task, newStatus); + if (result) + { + return await _repository.UpdateTaskItemAsync(task); + } + + return false; + } + + private bool ChangeStatus(TaskItem task, TaskItemStatus newStatus) + { switch (newStatus) { - case TaskStatus.ToDo: + case TaskItemStatus.ToDo: return task.Open(); - case TaskStatus.InProgress: + case TaskItemStatus.InProgress: return task.Start(); - case TaskStatus.Done: + case TaskItemStatus.Done: return task.Done(); default: return false; } - + } + + public async Task GetAllUsersAsync() => await _repository.GetAllUsersAsync(); + + public async Task AssignToAsync(int taskId, int? userId) + { + var task = await GetAsync(taskId); + if (task == null) + return false; + + User? user = null; + if (userId.HasValue) + { + user = await _repository.GetUserByIdAsync(userId.Value); + if (user == null) + return false; + } + task.AssignTo(user); + return await _repository.UpdateTaskItemAsync(task); } } } \ No newline at end of file diff --git a/TaskManager/BusinessLogic/User.cs b/TaskManager/BusinessLogic/User.cs new file mode 100644 index 0000000..b5c8864 --- /dev/null +++ b/TaskManager/BusinessLogic/User.cs @@ -0,0 +1,20 @@ +namespace TaskManager.BusinessLogic +{ + public class User + { + public int Id { get; } + public string Name { get; } + public List Tasks { get; } + + private User() { } + + public User(int id, string name) + { + Id = id; + Name = name; + Tasks = new List(); + } + + public override string ToString() => $"{Id}. {Name}"; + } +} \ No newline at end of file diff --git a/TaskManager/Program.cs b/TaskManager/Program.cs index 17b2dbf..f8dc406 100644 --- a/TaskManager/Program.cs +++ b/TaskManager/Program.cs @@ -1,20 +1,24 @@ -using System.Text; +using System.Data.SqlClient; +using System.Text; +using Dapper; using TaskManager.BusinessLogic; -//TaskStatus już istnieje w przestrzeni System.Threading.Tasks, która jest automatycznie importowana. -//Musimy rozwiązać konflikt nazw stosując alias. -using TaskStatus = TaskManager.BusinessLogic.TaskStatus; namespace TaskManager { public class Program { - private static TaskManagerService _taskManagerService = new TaskManagerService(); + private const string ConnectionString = "Server=localhost,1433;Initial Catalog=TaskManager;User ID=sa;Password=P@ssw0rd;Encrypt=True;TrustServerCertificate=True;Connection Timeout=30;"; - public static void Main() + private static TaskManagerService _taskManagerService = new TaskManagerService(new Repository(ConnectionString)); + private static int _createdBy = 1; + + public static async Task Main() { + // Console.WriteLine(await TestDbAsync()); string command; do { + Console.WriteLine("0. Wyświetl użytkowników"); Console.WriteLine("1. Dodaj zadanie"); Console.WriteLine("2. Usuń zadanie"); Console.WriteLine("3. Pokaż szczegóły zadania"); @@ -22,39 +26,56 @@ public static void Main() Console.WriteLine("5. Wyświetl zadania wg statusu"); Console.WriteLine("6. Szukaj zadania"); Console.WriteLine("7. Zmień status zadania"); - Console.WriteLine("8. Zakończ"); + Console.WriteLine("8. Przypisz zadanie"); + Console.WriteLine("9. Zakończ"); command = Console.ReadLine().Trim(); switch (command) { + case "0": + await DisplayAllUsersAsync(); + break; case "1": - AddTask(); + await AddTaskAsync(); break; case "2": - RemoveTask(); + await RemoveTaskAsync(); break; case "3": - ShowTaskDetails(); + await ShowTaskDetailsAsync(); break; case "4": - DisplayAllTasks(); + await DisplayAllTasksAsync(); break; case "5": - DisplayAllTasksByStatus(); + await DisplayAllTasksByStatusAsync(); break; case "6": - DisplaySearchedTasks(); + await DisplaySearchedTasksAsync(); break; case "7": - UpdateTaskStatus(); + await UpdateTaskStatusAsync(); + break; + case "8": + await AssignTaskAsync(); break; } Console.WriteLine(""); - } while (command != "8"); + } while (command != "9"); + } + + private static async Task DisplayAllUsersAsync() + { + var users = await _taskManagerService.GetAllUsersAsync(); + Console.WriteLine($"Jest {users.Length} użytkowników:"); + foreach (var user in users) + { + Console.WriteLine(user); + } } - private static void AddTask() + private static async Task AddTaskAsync() { Console.WriteLine("Podaj opis zadania:"); var description = Console.ReadLine(); @@ -67,11 +88,11 @@ private static void AddTask() dueDate = date; } - var task = _taskManagerService.Add(description, dueDate); + var task = await _taskManagerService.AddAsync(description, _createdBy, dueDate); WriteLineSuccess($"Dodano zadanie: {task}"); } - private static void RemoveTask() + private static async Task RemoveTaskAsync() { Console.WriteLine("Podaj identyfikator zadania do usunięcia:"); int taskId; @@ -80,7 +101,7 @@ private static void RemoveTask() Console.WriteLine("Podaj identyfikator zadania do usunięcia:"); } - if (_taskManagerService.Remove(taskId)) + if (await _taskManagerService.RemoveAsync(taskId)) { WriteLineSuccess($"Usunięto zadanie o numerze {taskId}"); } @@ -90,10 +111,10 @@ private static void RemoveTask() } } - private static void ShowTaskDetails() + private static async Task ShowTaskDetailsAsync() { var taskId = ReadTaskId(); - var task = _taskManagerService.Get(taskId); + var task = await _taskManagerService.GetAsync(taskId); if (task == null) { WriteLineError($"Nie można znaleźć zadania o numerze {taskId}"); @@ -103,6 +124,8 @@ private static void ShowTaskDetails() var sb = new StringBuilder(); sb.AppendLine(task.ToString()); sb.AppendLine($" Data utworzenia: {task.CreationDate}"); + sb.AppendLine($" Utworzone przez: {task.CreatedBy}"); + sb.AppendLine($" Przypisane do: {task.AssignedTo?.ToString() ?? ""}"); sb.AppendLine($" Data spodziewanego końca: {task.DueDate}"); sb.AppendLine($" Data startu: {task.StartDate}"); sb.AppendLine($" Data zakończenia: {task.DoneDate}"); @@ -110,35 +133,35 @@ private static void ShowTaskDetails() Console.WriteLine(sb); } - private static void DisplayAllTasks() + private static async Task DisplayAllTasksAsync() { - var tasks = _taskManagerService.GetAll(); - Console.WriteLine($"Masz {tasks.Length} zadań:"); + var tasks = await _taskManagerService.GetAllAsync(); + Console.WriteLine($"Jest {tasks.Length} zadań:"); foreach (var task in tasks) { Console.WriteLine(task); } } - private static void DisplayAllTasksByStatus() + private static async Task DisplayAllTasksByStatusAsync() { - var statuses = string.Join(", ", Enum.GetNames()); + var statuses = string.Join(", ", Enum.GetNames()); Console.WriteLine($"Podaj status: {statuses}"); - TaskStatus status; - while (!Enum.TryParse(Console.ReadLine(), true, out status)) + TaskItemStatus itemStatus; + while (!Enum.TryParse(Console.ReadLine(), true, out itemStatus)) { Console.WriteLine($"Podaj status: {statuses}"); } - var tasks = _taskManagerService.GetAll(status); - Console.WriteLine($"Masz {tasks.Length} zadań ({status}):"); + var tasks = await _taskManagerService.GetAllAsync(itemStatus); + Console.WriteLine($"Jest {tasks.Length} zadań ({itemStatus}):"); foreach (var task in tasks) { Console.WriteLine(task); } } - private static void DisplaySearchedTasks() + private static async Task DisplaySearchedTasksAsync() { Console.WriteLine($"Wyszukaj zadania o treści (możesz podać fragment):"); string text; @@ -152,7 +175,7 @@ private static void DisplaySearchedTasks() } break; } - var tasks = _taskManagerService.GetAll(text); + var tasks = await _taskManagerService.GetAllAsync(text); Console.WriteLine($"Znaleziono {tasks.Length} zadań:"); foreach (var task in tasks) { @@ -160,18 +183,18 @@ private static void DisplaySearchedTasks() } } - private static void UpdateTaskStatus() + private static async Task UpdateTaskStatusAsync() { var taskId = ReadTaskId(); - var statuses = string.Join(", ", Enum.GetNames()); + var statuses = string.Join(", ", Enum.GetNames()); Console.WriteLine($"Podaj nowy status: {statuses}"); - TaskStatus status; - while (!Enum.TryParse(Console.ReadLine(), true, out status)) + TaskItemStatus itemStatus; + while (!Enum.TryParse(Console.ReadLine(), true, out itemStatus)) { Console.WriteLine($"Podaj nowy status: {statuses}"); } - if (_taskManagerService.ChangeStatus(taskId, status)) + if (await _taskManagerService.ChangeStatusAsync(taskId, itemStatus)) { WriteLineSuccess($"Zmieniono status zadania o numerze {taskId}"); } @@ -181,6 +204,21 @@ private static void UpdateTaskStatus() } } + private static async Task AssignTaskAsync() + { + var taskId = ReadTaskId(); + var userId = ReadUserId(); + + if (await _taskManagerService.AssignToAsync(taskId, userId)) + { + WriteLineSuccess($"Przypisano zadanie o numerze {taskId} do użytkownika {userId}"); + } + else + { + WriteLineError($"Nie można przypisać zadania o numerze {taskId} do użytkownika {userId}"); + } + } + private static int ReadTaskId() { Console.WriteLine("Podaj identyfikator zadania:"); @@ -192,6 +230,21 @@ private static int ReadTaskId() return taskId; } + private static int? ReadUserId() + { + while (true) + { + Console.WriteLine("Podaj identyfikator użytkownika lub pozostaw puste, aby odpiąć użytkownika od zadania:"); + var str = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(str)) + return null; + + int userId; + if (int.TryParse(str, out userId)) + return userId; + } + } + private static void WriteLineSuccess(string text) { Console.ForegroundColor = ConsoleColor.Green; @@ -205,5 +258,20 @@ private static void WriteLineError(string text) Console.WriteLine(text); Console.ResetColor(); } + + private static async Task TestDbAsync() + { + using (var connection = new SqlConnection(ConnectionString)) + { + var sql = @"SELECT CONCAT( + 'Tabela TaskItems ' + , CASE WHEN OBJECT_ID('TaskItems', 'U') IS NOT NULL THEN 'istnieje' ELSE 'nieistnieje' END + , CHAR(13)+CHAR(10) + , CONCAT('Tabela Users ', CASE WHEN OBJECT_ID('Users', 'U') IS NOT NULL THEN 'istnieje' ELSE 'nieistnieje' END) +)"; + var result = await connection.QueryFirstAsync(sql); + return result; + } + } } } \ No newline at end of file diff --git a/TaskManager/TaskManager.csproj b/TaskManager/TaskManager.csproj index b9de063..eb91a06 100644 --- a/TaskManager/TaskManager.csproj +++ b/TaskManager/TaskManager.csproj @@ -7,4 +7,9 @@ enable + + + + + From aabe5e2268d2cc49366fdf05deecf6cd81b31670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Wasilewski-CL?= Date: Thu, 21 Mar 2024 06:25:56 +0100 Subject: [PATCH 2/7] MOD Readme --- README.md | 735 ++---------------------------------------------------- 1 file changed, 16 insertions(+), 719 deletions(-) diff --git a/README.md b/README.md index e584876..1204b17 100644 --- a/README.md +++ b/README.md @@ -1,727 +1,24 @@ ![Coders-Lab-1920px-no-background](https://user-images.githubusercontent.com/30623667/104709387-2b7ac180-571f-11eb-9b94-517aa6d501c9.png) -> Jest to gotowa - wzorcowa wersja warsztatu TaskManager z Dapper. +# Kilka ważnych informacji -## Wstęp do warsztatu TaskManager z Dapper +Przed przystąpieniem do rozwiązywania zadań przeczytaj poniższe wskazówki. -Celem dzisiejszego warsztatu jest rozbudowa aplikacji TaskManager. Jest to aplikacja do zarządzania i planowania zadań do wykonania. Aplikacja będzie składała się z pięciu części: -- logiki biznesowej (modele), -- aplikacji (serwis), -- bazy danych, -- testów, -- interfejsu użytkownika (w postaci konsoli). +## Jak zacząć? -Podczas warsztatów rozbudujesz projekt o nowe funkcjonalności oraz integrację z bazą danych przy pomocy paczki NuGet `Dapper`. +1. Sklonuj oryginalne repozytorium na swój komputer (nie klonuj forka). Użyj do tego komendy `git clone adres_repozytorium` +Adres możesz znaleźć na stronie repozytorium po naciśnięciu w guzik "Clone or download". +2. Przełącz się na branch `gitflow/NUMER_KURSU` tego repozytorium (w miejsce NUMER_KURSU podstaw rzeczywistą sygnaturę kursu). +3. Utwórz właśny unikalny branch `feature/X`, wstawiając za `X` unikalną nazwę, np. numer kursu i Twoje inicjały z liczbą. +4. Rozwiąż zadania i skomituj zmiany do swojego brancha z punktu 3. Użyj do tego komend `git add nazwa_pliku`. +Jeżeli chcesz dodać wszystkie zmienione pliki użyj `git add .` +Pamiętaj że kropka na końcu jest ważna! +Następnie skommituj zmiany komendą `git commit -m "nazwa_commita"` +5. Wypchnij zmiany do swojego brancha w repozytorium na GitHubie. Użyj do tego komendy `git push origin feature/` (podstaw za `X` Twoją nazwę brancha). +6. Stwórz [*pull request*](https://help.github.com/articles/creating-a-pull-request) z Twojego brancha do brancha `gitflow`, gdy skończysz wszystkie zadania. -Pamiętaj, że programista to nie jest "zwykły klepacz kodu" tylko pracujący kreatywnie rzemieślnik i inżynier dbający o logikę działania aplikacji oraz jej całą konstrukcję. Aby dobrze wykonywać swoją pracę należy używać dobrych praktyk progamistycznych oraz projektowych. Podział projektu na warstwy ze względu na odpowiedzialność kodu jest jedną z takich praktyk. Współcześnie projektuje się tzw. aplikacje N-wartstwowe. +Poszczególne zadania rozwiązuj w odpowiednich plikach. -Z tego powodu dodatkowym celem warsztatów jest podział aplikacji na warstwy wg odpowiedzialności. Docelowo aplikacja na koniec warsztatów będzie składała się z czterech warstw: -- warstwy logiki biznesowej (modele), -- warstwy aplikacji (serwis), -- warstwy prezentacji (konsola), -- warstwy dostępu do danych (repozytorium). +### Poszczególne zadania rozwiązuj w odpowiednich plikach. -Idea działania jest następująca: -- użytkownik korzysta z warstwy prezentacji (konsola), -- program konsolowy korzysta z warstwy aplikacji (serwis), -- serwis korzysta z warstwy logiki biznesowej (modeli) i dostępu do danych (repozytorium), -- logika biznesowa w modelach jest główną częścią "biznesu", -- repozytorium korzysta z bazy danych do przechowywania stanu model biznesowych. - -Dzięki modularnej budowie aplikacja staje się niezależna od swoich modułów/klocków. Wyobraź sobie sytuację, w której należy wymienić bazę danych na inną. Wówczas nie musisz zmieniać całej aplikacji. Wystarczy przepiąć moduł dostępu do danych, a pozostałe warstwy korzystające z niego nawet tego nie zauważą :) Co więcej w dalszej części kursu możesz wykorzystać obecny projekt to tego, aby użyć innej warstwy prezentacji danych i zamiast konsoli komunikować się poprzez API, które potem będzie komunikowało się ze stroną internetową. - -Dzięki sprytnemu zabiegowi i podziału kodu na mniejsze części programista jest gotowy na bezgraniczny rozwój aplikacji. Dokonywanie modyfikacji w aplikacji będzie łatwiejsze, a co najważniejsze, będzie można dzielić się pracą z innymi programistami tak, aby nie mieszać swoich wersji kodu. - -W kolejnych artykułach dowiesz się jakie są wymogi działania aplikacji. - -### Czego nauczysz się podczas tego warsztatu? - -Warsztat jest w formie wykonania jednego dużego zadania jakim jest rozbudowa projektu o integrację z bazą danych. Nauczysz się jak rozwijać swoją aplikację, porządkować ją i rozbudowywać o kolejne moduły i funkcje. Na pewno da to duży zastrzyk praktycznej wiedzy i pozwala na szybsze i bardziej pewne poruszanie się po narzędziach deweloperskich i kodzie C#. - -Nauczysz się podczas warsztatów, że rozbudowa aplikacji wymaga czasami dostosowania jej działania do nowych wymogów i potrzeb, a to wiąże się z procesem tzw. refaktoru kodu. Dzięki temu poczujesz namiastkę pracy programisty w firmie. - -W projekcie tym użyjesz praktycznie wszystkich rzeczy, o których mówiliśmy podczas tego modułu takie jak: - - baza danych MS SQL Server, - - tworzenie zapytań SQL, - - użycie mikro ORM Dapper. - -Wszystko to będzie możliwe do zastosowania w tym projekcie! To na pewno ugruntuje Twoją wiedzę. - ---- ---- ---- ---- - -## Zakres funkcjonalności logiki biznesowej aplikacji TaskManager z Dapper - -**Rozszerzone założenia logiki biznesowej:** -- System umożliwia pobieranie użytkowników z zadaniami. -- Użytkownik posiada cechy: ID, nazwę, lista zadań. System posiada predefiniowanych użytkowników. -- Zadanie posiada dodatkowe pola: twórca zadania (jedna osoba) i osoba przypisana do zadania (jedna osoba). -- System pozwala na zarządzanie zadaniami: przypisywanie zadania do użytkownika. -- System umożliwia przechowywanie w bazie danych informacji o zadaniach, użytkownikach i ich relacji (powiązaniu). -- Jedno zadanie może mieć wyłącznie jednego przypisanego użytkownika. -- Jeden użytkownik może posiadać wiele przypisanych do siebie zadań. - -> Pamiętaj, aby wszystkie metody komunikujące się z bazą danych były wywoływane asynchronicznie (`async`/`await`). - ---- - -### **1. Zmiana nazw typów (refaktor nazw)** - -Przed przystąpieniem do rozbudowy aplikacji warto przeznaczyć czas na dostosowanie i uporządkowanie aplikacji, czyli na refaktor. - -Aplikacja posiada kilka nazw tj. `Task`, `TaskStatus`, które tworzą konflikt nazw z systemowymi zadaniami związanymi z programowaniem asynchronicznym. W trakcie warsztatów będziemy używali tych samych nazw dla typu naszego zadania, jak i zadania w rozumieniu asynchroniczności. Najlepiej rozwiązać ten problem zmieniając nazwę naszego zadania i statusu. - -**Jeżeli w poprzednich warsztatach wybrano inne nazwy dla tych dwóch typów, to możesz pominąć tę część.** - -Sugerujemy, aby nasze zadanie miało nazwę `TaskItem`. Do refaktoru nazw użyjemy polecenia `Rename` (skrót `F2` lub `CTRL+R+R`). Ułatwi to dostosowanie kodu, ponieważ IDE programistyczne automatycznie powinno zmienić nazwę klasy i pliku oraz użycia klasy jak i zmienne. - -**Refaktor `Program`:** -- Zmień metodę `Main` tak, aby była asynchroniczna. Zmień `void` na `async Task`. - -**Refaktor `Task`:** -- Zmień nazwę klasy `Task` na `TaskItem`. - -**Refaktor `TaskStatus`:** -- Zmień nazwę enum `TaskStatus` na `TaskItemStatus`. - -**Refaktor `TaskTests`:** -- Zmień nazwę klasy `TaskTests` na `TaskItemTests`. - -**Refaktor `TaskManagerService`:** -- Wprowadź asynchroniczność do każdej metody. Zmień nazwę każdej metody dodając sufiks `Async` (użyj polecenia `Rename`). Dodaj słowo kluczowe `async` oraz wykorzystaj `Task`, a jeżeli metoda zwraca wartość to użyj `Task<>`. -- Wyszukaj każde wywołanie tych metod i dopisz obsługę `await` przy wywołaniu i upewnij się, że metoda wywołująca również jest asynchroniczna. Dostosuj wywołanie w klasach `TaskManagerService`, `TaskManagerServiceTests` oraz `Program`. - -> Możesz wykorzystać w IDE polecenia do globalnego szukania tekstu `Find all` (skrót `CTRL+SHIFT+F`), aby poszukać tekst `Async(`. IDE wyświetli wszystkie miejsca, gdzie jest użyt ten tekst. Przejdź przez listę i dostosuj wywołania metod. - -**Weryfikacja:** -- Po refaktorze należy zweryfikować poprawność działania aplikacji. -- W pierwszej kolejności przebuduj solucję, aby sprawdzić, czy nie ma błędów kompilacji. Jeżeli jakieś błędy wystąpią, zbadaj je i rozwiąż. Pamiętaj, że możesz sprawdzić w internecie co mogą oznaczać błędy, a także możesz zapytać na Slacku. -- Następnie uruchom wszystkie testy jednostkowe `TaskItemTests` i `TaskManagerServiceTests`. Wszystkie testy powinny przechodzić (mieć zielony kolor). Jeżeli testy dają błędny wynik, to sprawdź dlaczego i napraw testy. Jeżeli metoda testowa do sprawdzania autoinkrementacji zadania nie przechodzi, to zakomentuj ten test. Nie będzie nam potrzebny w tych warsztatach. -- Na końcu uruchom aplikację konsolową i przetestuj jej działanie. Jeżeli w aplikacji zauważysz jakieś błędy w działaniu, to napraw je. - -**Jeżeli refaktor przebiegł pomyślnie, przejdź do dalszej części.** - ---- - -### **2. Utworzenie typu `User`** - -Reprezentuje pojedynczego użytkownika. -Utwórz klasę `User` w folderze `BusinessLogic`. - -**Cechy:** -- `Id`: Unikalny identyfikator użytkownika w formie `int`. -- `Name`: Nazwa użytkownika (wartość wymagana). -- `Zadania`: Lista przypisanych zadań. - -**Akcje:** -- Domyślny konstruktor bez parametrów: Utwórz **prywatny** konstruktor bez parametrów. Jest to obejście niezbędne do prawidłowego mapowania danych w ORM. -- Konstruktor z parametrami: Tworzy obiekt użytkownika na podstawie dostarczonego identyfikatora oraz nazwy. Lista zadań ma być utworzona i pusta. -- `ToString`: Użyj własnej wersji metody do wyświetlania informacji o użytkowniku w formacie `ID. Nazwa`. - ---- - -### **3. Rozszerzenie `TaskItem`** - -Reprezentuje pojedyncze zadanie. - -**Dodatkowe cechy:** -- `CreatedBy`: Twórca zadania (`User`, wartość wymagana), tylko do odczytu (bez settera) z prywatnym polem o nazwie `_createdBy`. -- `AssignedTo`: Osoba przypisana do wykonania zadania (`User`, wartość opcjonalna, na początku równa `null`), tylko do odczytu (bez settera) z prywatnym polem o nazwie `_assignedTo`. - -Niestety, ale ORM Dapper nie radzi sobie z mapowaniem właściwości do odczytu (bez settera), które pobrano przy użyciu łaczenia tabel (JOIN), które mają własny typ danych (inny niż systemowe). Z tego powodu, aby zachować hermetyzację danych i "ukryć przed światem" modyfikacje wartości, musimy zastosować obejście w postaci wprowadzenia prywatnego pola, które będzie przechowywało właściwą informację, a właściwość będzie tylko wyświetlała tę wartość. O tym jak będziemy używać tego obejścia wyjaśnimy później. - -**Dodatkowe akcje:** -- Domyślny konstruktor bez parametrów: Utwórz **prywatny** konstruktor bez parametrów. Jest to obejście niezbędne do prawidłowego mapowania danych w ORM. -- Dostosuj konstruktor z parametrami: Tworzy obiekt zadania na podstawie dostarczonego identyfikatora, opisu zadania, twórcy zadania oraz opcjonalnej daty zakończenia zadania. -- Dostosuj metodę `ToString`: niech metoda zwraca dodatkowo informację o przypisanym użytkowniku do zadania (o ile taki istnieje). -- Usuń mechanizm autoinkrementacji z modelu zadania. Identyfikator będzie przekazywany w konstruktorze. Na razie mechanizm nadawania ID zadaniom przejmie `TaskManagerService` (o tym za chwilę), a docelowo baza danych (o tym później). -- `AssignTo(User? assignedTo)`: Przypisuje użytkownika do zadania. Jeżeli użytkownik jest `null` to ma "odpiąć użytkownika od zadania" i ustawić wartość `null`. Metoda zwraca `void`. - ---- - -### **4. Rozszerzenie `TaskManagerService`** - -Reprezentuje serwis przechowujący i zarządzający listą zadań. - -**Dodatkowe cechy:** -- `_id`: Prywatna statyczna zmienna typu `int` o początkowej wartości `0`. - -**Zmienione akcje:** -- `AddAsync(description, createdBy, dueDate)`: Dodaje nowe zadanie do listy zadań z podanym opisem, ID twórcy zadania i opcjonalną datą realizacji. Tworząc nowy obiekt zadania przekaż parametr użytkownika `User` (utwórz go podająć ID createdBy i wpisz dowolną nazwę, nie jest to istotne w tym momencie). Zwraca utworzone zadanie. Tworząc nowe zadanie metoda zwiększaj licznik `_id` preinkrementując go. - ---- - -### **5. Dostosowanie testów `TaskItemTests`** - -1. Utwórz w klasie testowej prywatnego użytkownika `User` z przykłdowymi danymi, np. -```csharp -private User _createdBy = new User(1, "Ja"); -``` -2. Uzupełnij w każdej metodzie testowej wywołanie konstruktora `TaskItem` o wartość `id` oraz `createdBy`. Jako wartość `id` podaj dowolnie wymyśloną wartość, np. `1`, `2`, itd. Natomiast jako twórcę zadania przekaż `_createdBy`. -3. Nie uruchamiaj jeszcze testów, należy jeszcze dostosować testy `TaskManagerServiceTests`. - ---- - -### **6. Dostosowanie testów `TaskManagerServiceTests`** - -1. Utwórz w klasie testowej prywatne ID użytkownika, np. -```csharp -private readonly int _createdBy = 1; -``` -2. Uzupełnij w każdej metodzie testowej wywołanie metody `AddAsync` o wartość `createdBy`. Użyj zmiennej `_createdBy`. -3. Nie uruchamiaj jeszcze testów, należy jeszcze dostosować aplikację konsolową `Program`. - ---- - -### **7. Dostosowanie aplikacji konsolowej `Program`** - -1. Utwórz w klasie programu prywatne, statyczne ID użytkownika, np. -```csharp -private static int _createdBy = 1; -``` -2. Uzupełnij wywołanie metody `TaskManagerService.AddAsync` o wartość `createdBy`. Użyj zmiennej `_createdBy`. -3. Przekompiluj solucję. Uruchom wszystkie testy oraz aplikację. Jeżeli testy przechodzą oraz aplikacja działa, przejdź dalej, jeżeli nie to napraw powstałe błędy i przejrzyj jeszcze raz instrukcję. - ---- - -### **8. Utworzenie bazy danych `TaskManager`** - -Baza danych do przechowywania informacji o zadaniach i użytkownikach. -Napisz skrypt SQL do utworzenia kompletnej bazy danych z tabelami i relacjami. - -**Utwórz bazę danych `TaskManager`:** -- Napisz skrypt do utworzenia bazy danych `TaskManager`. -- Uruchom skrypt na bazie danych MS SQL Server. -- Ustaw nową bazę danych jako domyślną do użycia w kolejnych zapytaniach. - -**Utwórz tabelę `Users`:** -- `Id`: klucz główny o typie `INT`, automatycznie numerowany `IDENTITY(1,1)`. -- `Name`: kolumna typu `NVARCHAR(MAX)`, wymagana. -- Uruchom skrypt na bazie danych MS SQL Server. - -**Utwórz tabelę `TaskItems`:** -- `Id`: klucz główny o typie `INT`, automatycznie numerowany `IDENTITY(1,1)`. -- `Description`: kolumna typu `NVARCHAR(MAX)`, wymagana. -- `CreationDate`: kolumna typu `DATETIME`, wymagana. -- `DueDate`: kolumna typu `DATETIME`, niewymagana. -- `StartDate`: kolumna typu `DATETIME`, niewymagana. -- `DoneDate`: kolumna typu `DATETIME`, niewymagana. -- `Status`: kolumna typu `INT`, wymagana. -- `CreatedById`: klucz obcy typu `INT` do tabeli `Users`, wymagany. -- `AssignedToId`: klucz obcy typu `INT` do tabeli `Users`, niewymagany. -- Uruchom skrypt na bazie danych MS SQL Server. - -**Dodaj predefiniowanych użytkowników:** -- Napisz skrypt i uruchom go, aby dodać predefiniowanych użytkowników: - - W pierwszej kolejności dodaj użytkownika z Twoim imieniem i nazwiskiem. - - Dodaj dwóch innych użytkowników, np. `Anna Pawlak`, `Jan Nowak`. -- Upewnij się, że masz co najmniej trzech użytkowników w bazie danych. - ---- - -### **9. Konfiguracja Dapper i połączenia z bazą danych** - -1. Zainstaluj w głównym projekcie `TaskManager` paczki NuGet: `Microsoft.Data.SqlClient` i `Dapper`. -2. W aplikacji konsolowej w klasie `Program` utwórz prywatną stałą `ConnectionString`. Pamiętaj, aby konfiguracja wskazywała na użycie nowej bazy danych `TaskManager`. -3. Dla testu i poprawności działania bazy danych utwórz w klasie `Program` prywatną statyczną metodę `TestDbAsync` i wywołaj ją na początku metody `Main`. Użyj kodu dostarczonego poniżej. -4. Jeżeli test przejdzie pozytywnie to usuń wywołanie metody. - -
-Pokaż kod TestDbAsync - -```csharp -private static async Task TestDbAsync() -{ - using (var connection = new SqlConnection(ConnectionString)) - { - var sql = @"SELECT CONCAT( -'Tabela TaskItems ' -, CASE WHEN OBJECT_ID('TaskItems', 'U') IS NOT NULL THEN 'istnieje' ELSE 'nieistnieje' END -, CHAR(13)+CHAR(10) -, CONCAT('Tabela Users ', CASE WHEN OBJECT_ID('Users', 'U') IS NOT NULL THEN 'istnieje' ELSE 'nieistnieje' END) -)"; - var result = await connection.QueryFirstAsync(sql); - return result; - } -} -``` -
- ---- - -### **10. Utworzenie szablonu repozytorium do komunikacji z bazą danych** - -Repozytorium (*ang. repository*) to koncept/wzorzec projektowy dla klasy komunikującej się z bazą danych. - -Klasa zawiera wyłącznie metody, które pobierają lub modyfikują dane w bazie danych. -Jej zadaniem jest rozdzielenie komunikacji z bazą danych od logiki biznesowej. - -Do zarządzania logiką biznesową służą tzw. domeny (*ang. domain*) czyli modele biznesowe oraz serwisy (*ang. services*). Modele domenowe przechowują informacje biznesowe oraz zarządzają dostępem do informacji (są odpowiedzialne za hermetyzację). Serwisy łączą/integrują modele biznesowe z zewnętrznymi systemami, np. bazami danych, API, itd. - -Repozytorium powinno być klasą implementującą interfejs `IRepository`. Interfejs służy temu, aby posiadać wiele implementacji repozytorium. Projekt z aplikacją konsolową może używać bazy danych, natomiast projekt z testami niekoniecznie. Testy są uruchamiane często i nie powinny mieć dostępu do głównej bazy danych. Zazywczaj testy pomijają komunikację z bazą danych poprzez użycie obiektu typu **mock** lub posiadają własną bazę danych tworzoną na żądanie, np. przy użyciu Dockera lub przechowującą dane w pamięci. - -W warsztatach w projekcie testów wykorzystamy prostą implementację `IRepository` w formie mocka. Będzie ona dostarczona w formie gotowej klasy do przekopiowania. Nie chcemy, aby uruchamianie testów powodowało zmiany w naszej głównej bazie danych. - -Zwróć uwagę, że odkąd będziemy używać repozytorium to metody serwisu będą hermetyczne, tj. dane zawsze będą aktualne i pobierane z bazy danych. W przypadku modyfikacji danych będzie następujący przepływ: -- Serwis najpierw pobierze aktualne dane z bazy przy pomocy repozytorium, które zwróci w formie modelu biznesowego, np. `TaskItem`. -- Następnie będziemy dokonywali modyfikacji tego modelu przy pomocy metod w klasie `TaskItem`. -- Efekt zmian wyślemy do repozytorium, aby zapisać je w bazie danych. - -**UWAGA: Należy pamiętać, że Dapper jest mikro ORM-em, a więc jeżeli dokonamy zmian w modelu to zmiany będą widoczne w bazie danych dopiero kiedy wprost wywołamy metody modyfikacji w repozytorium.** - ---- - -#### **Utwórz interfejs `IRepository` w folderze `BusinessLogic` z akcjami:** -- `GetAllUsersAsync()`: do pobierania wszystkich użytkowników. -- `GetUserByIdAsync(int userId)`: do pobierania użytkownika i podanym ID. -- `CreateTaskItemAsync(TaskItem newTaskItem)`: do tworzenia zadania, metoda ma zwracać ID utworzonego zadania. -- `UpdateTaskItemAsync(TaskItem newTaskItem)`: do aktualizacji zadania, zwraca informację czy aktualizacja powiodła się. -- `DeleteTaskItemAsync(int taskItemId)`: do usuwania zadania o podanym ID, zwraca informację czy usuwanie powiodło się. -- `GetTaskItemByIdAsync(int taskItemId)`: pobiera zadanie o podanym ID. -- `GetAllTaskItemsAsync()`: pobiera wszystkie zadania. -- `GetTaskItemsByStatusAsync(TaskItemStatus status)`: pobiera wszyskie zadania o podanym statusie. -- `GetTaskItemsByDescriptionAsync(string description)`: pobiera wszystkie zadania, w których występuje podana fraza w opisie. - -
-Pokaż kod źródłowy interfejsu IRepository - -```csharp -public interface IRepository -{ - Task GetAllUsersAsync(); - Task GetUserByIdAsync(int userId); - Task CreateTaskItemAsync(TaskItem newTaskItem); - Task UpdateTaskItemAsync(TaskItem newTaskItem); - Task DeleteTaskItemAsync(int taskItemId); - Task GetTaskItemByIdAsync(int taskItemId); - Task GetAllTaskItemsAsync(); - Task GetTaskItemsByStatusAsync(TaskItemStatus status); - Task GetTaskItemsByDescriptionAsync(string description); -} -``` -
- ---- - -#### **Utwórz klasę `Repository` w folderze `BusinessLogic`:** -- Zaimplementuj interfejs `IRepository` domyślnym zachowaniem, tak aby wywołanie każdej metody wyrzucało wyjątek. W tym celu w IDE wywołaj polecenie `Implement missing members` lub użyj gotowego kodu dostarczonego poniżej. -- Dodaj konstruktor przyjmujący parametr `connectionString` i zachowaj jego wartość w zmiennej tylko do odczytu `_connectionString`. Użyjemy tego później. - -
-Pokaż domyślną implementację Repository - -```csharp -public class Repository : IRepository -{ - private readonly string _connectionString; - - public Repository(string connectionString) - { - _connectionString = connectionString; - } - - public Task GetAllUsersAsync() - { - throw new NotImplementedException(); - } - - public Task GetUserByIdAsync(int userId) - { - throw new NotImplementedException(); - } - - public Task CreateTaskItemAsync(TaskItem newTaskItem) - { - throw new NotImplementedException(); - } - - public Task UpdateTaskItemAsync(TaskItem newTaskItem) - { - throw new NotImplementedException(); - } - - public Task DeleteTaskItemAsync(int taskItemId) - { - throw new NotImplementedException(); - } - - public Task GetTaskItemByIdAsync(int taskItemId) - { - throw new NotImplementedException(); - } - - public Task GetAllTaskItemsAsync() - { - throw new NotImplementedException(); - } - - public Task GetTaskItemsByStatusAsync(TaskItemStatus status) - { - throw new NotImplementedException(); - } - - public Task GetTaskItemsByDescriptionAsync(string description) - { - throw new NotImplementedException(); - } -} -``` -
- ---- - -#### **Użyj interfejsu `IRepository` w klasie `TaskManagerService`:** -- W klasie `TaskManagerService` utwórz prywatną zmienną tylko do odczytu `_repository` typu `IRepository`. -- Utwórz konstruktor `TaskManagerService` z parametrem `IRepository` i przekaż wartość do zmiennej `_repository`. Dzięki temu będziemy mogli używać serwisu zarówno w aplikacji konsolowej (w wersji z prawdziwą bazą danych) jak i w testach (wersja mock). -- Usuń dwie zmienne z klasy: z użyciem autoinkrementowanego ID dla zadań oraz listę zadań. Zastąpimy ich użyciem repozytorium. -- W metodzie `AddAsync` zastąp użcie autoinkrementowanego ID wartością `0`. -- We wszystkich metodach serwisu zastąp użycie metod na liście `_tasks`, odpowiednią metodą z repozytorium. Pamiętaj o użyciu `async/await`: - - `AddAsync`: na początku pobierz użytkownika metodą `_repository.GetUserByIdAsync` i wstaw go . Następnie zastąp użycie `_tasks.Add` wywołaniem `_repository.CreateTaskItemAsync`. Rezultat wywołania metody z repozytorium wykorzystaj do pobrania pełnego obiektu z bazy danych i zwróć jej wynik jako rezultat `AddAsync`. Do pobrania pełnego obiektu możesz użyć metody `GetAsync`. Docelowo metoda repozytorium zwróci użytkownika z uzupełnionym ID. - - `RemoveAsync`: zastąp użycie `_tasks.Remove` wywołaniem `_repository.DeleteTaskItemAsync` z parametrem ID zadania. - - `GetAsync`: zastąp użycie `_tasks.Find` wywołaniem `_repository.GetTaskItemByIdAsync`. - - `GetAllAsync`: zastąp użycie `_tasks.ToArray()` wywołaniem `_repository.GetAllTaskItemsAsync()`. - - `GetAllAsync(TaskItemStatus)`: zastąp użycie `_tasks.FindAll` wywołaniem `_repository.GetTaskItemsByStatusAsync`. - - `GetAllAsync(string)`: zastąp użycie `_tasks.FindAll` wywołaniem `_repository.GetTaskItemsByDescriptionAsync`. - - `ChangeStatusAsync`: dostosuj wywołanie metody, tak aby wynik wywołanie metod modelu `TaskItem` (np. `Open`, `Start`, `Done`) przechować w zmiennej pomocniczej. Następnie jeżeli udało się zmienić status, to należy zapisać zmiany w bazie danych poprzez wywołanie `_repository.UpdateTaskItemAsync` i przekazując aktualną wersję obiektu `TaskItem`. Metoda `ChangeStatusAsync` powinna zwracać rezultat wywołania metody z repozytorium, a jeżeli nie było to możliwe to ma zwrócić `false`. - -
-Pokaż kod źródłowy TaskManagerService - -```csharp -public class TaskManagerService -{ - private readonly IRepository _repository; - - public TaskManagerService(IRepository repository) - { - _repository = repository; - } - - public async Task AddAsync(string description, int createdBy, DateTime? dueDate) - { - var user = await _repository.GetUserByIdAsync(createdBy); - var task = new TaskItem(0, description, user, dueDate); - var id = await _repository.CreateTaskItemAsync(task); - return await GetAsync(id); - } - - public async Task RemoveAsync(int taskId) - { - var task = await GetAsync(taskId); - if (task != null) - return await _repository.DeleteTaskItemAsync(task.Id); - return false; - } - - public async Task GetAsync(int taskId) - { - return await _repository.GetTaskItemByIdAsync(taskId); - } - - public async Task GetAllAsync() - { - return await _repository.GetAllTaskItemsAsync(); - } - - public async Task GetAllAsync(TaskItemStatus itemStatus) - { - return await _repository.GetTaskItemsByStatusAsync(itemStatus); - } - - public async Task GetAllAsync(string description) - { - return await _repository.GetTaskItemsByDescriptionAsync(description); - } - - public async Task ChangeStatusAsync(int taskId, TaskItemStatus newStatus) - { - var task = await GetAsync(taskId); - if (task == null || task?.Status == newStatus) - return false; - - var result = ChangeStatus(task, newStatus); - if (result) - { - return await _repository.UpdateTaskItemAsync(task); - } - - return false; - } - - private bool ChangeStatus(TaskItem task, TaskItemStatus newStatus) - { - switch (newStatus) - { - case TaskItemStatus.ToDo: - return task.Open(); - case TaskItemStatus.InProgress: - return task.Start(); - case TaskItemStatus.Done: - return task.Done(); - default: - return false; - } - } -} -``` -
- ---- - -#### **W klasie `Program` dostosuj tworzenie obiektu `TaskManagerService`:** -- Poszukaj linii kodu z tworzeniem obiektu `TaskManagerService` i przekaż w jego konstruktorze obiekt repozytorium z konfiguracją połączenia z bazą danych `new Repository(ConnectionString)`. Użyjemy tego później, ale na ten moment potrzebujemy działającego kodu. - ---- - -#### **W projekcie testów utwórz klasę `MockRepository`:** -- Zaimplementuj interfejs `IRepository` udający połączenie z bazą danych. W tym celu wykorzystaj dostarczony kod poniżej. -- `MockRepository` robi to co w pierwotnej wersji robił serwis, czyli przechowuje w pamięci listę zadań i umożliwia zarządzanie nimi na potrzeby testów. - -
-Pokaż implementację MockRepository - -```csharp -public class MockRepository : IRepository -{ - private int _taskId = 0; - private List _tasks = new List(); - - private List _users = new List { new User(1, "Ja") }; - - public async Task GetAllUsersAsync() => _users.ToArray(); - - public async Task GetUserByIdAsync(int userId) => _users.FirstOrDefault(u => u.Id == userId); - - public async Task CreateTaskItemAsync(TaskItem newTaskItem) - { - var newTask = new TaskItem(newTaskItem.Id == 0 ? ++_taskId : newTaskItem.Id, newTaskItem.Description, newTaskItem.CreatedBy, newTaskItem.DueDate); - _tasks.Add(newTask); - return newTask.Id; - } - - public async Task UpdateTaskItemAsync(TaskItem newTaskItem) - { - var result = await DeleteTaskItemAsync(newTaskItem.Id); - if (result) - _tasks.Add(newTaskItem); - return result; - } - - public async Task DeleteTaskItemAsync(int taskItemId) - { - var task = await GetTaskItemByIdAsync(taskItemId); - return _tasks.Remove(task); - } - - public async Task GetTaskItemByIdAsync(int taskItemId) => _tasks.Find(t => t.Id == taskItemId); - - public async Task GetAllTaskItemsAsync() => _tasks.ToArray(); - - public async Task GetTaskItemsByStatusAsync(TaskItemStatus status) => _tasks.Where(t => t.Status == status).ToArray(); - - public async Task GetTaskItemsByDescriptionAsync(string description) => - _tasks.FindAll(t => t.Description.Contains(description, StringComparison.InvariantCultureIgnoreCase)).ToArray(); -} -``` -
- ---- - -#### **Użyj `MockRepository` w projekcie testów:** -- Przejdź do klasy `TaskManagerServiceTests` i dostosuj tworzenie obiektu `TaskManagerService` poprzez dodanie w konstruktorze `new MockRepository()`. - ---- - -#### **Weryfikacja:** -- Po wstępnej rozbudowie i małym refaktorze należy zweryfikować poprawność działania aplikacji. -- W pierwszej kolejności przebuduj solucję, aby sprawdzić, czy nie ma błędów kompilacji. Jeżeli jakieś błędy wystąpią, zbadaj je i rozwiąż. Pamiętaj, że możesz sprawdzić w internecie co mogą oznaczać błędy, a także możesz zapytać na Slacku. -- Następnie uruchom wszystkie testy jednostkowe `TaskItemTests` i `TaskManagerServiceTests`. Wszystkie testy powinny przechodzić (mieć zielony kolor). Jeżeli testy dają błędny wynik, to sprawdź dlaczego i napraw testy. Jeżeli metoda testowa do sprawdzania autoinkrementacji zadania nie przechodzi, to zakomentuj ten test. Nie będzie nam potrzebny w tych warsztatach. -- Nie testuj działania aplikacj, ponieważ użyliśmy w niej repozytorium, w którym wszystkie metody zgłaszają wyjątek `throw new NotImplementedException();`. - ---- - -### **11. Utworzenie metod repozytorium `Repository` ze skryptami SQL** - -Kiedy mamy utworzony szablon klasy repozytorium, możemy przystąpić do implementacji poszczególnych metod wykorzystując Dapper i MS SQL. - -W tej sekcji będziemy implementować metody w klasie `Repository`. Pamiętaj, aby w każdej metodzie najpierw nawiązać nowe połączenie z bazą danych. - -**UWAGA:** w tym miejscu będziemy musieli zastosować wcześniej wspomniane obejście, aby prawidłowo mapować dane zadania związane z twórcą zadania (`CreatedBy`) i osobą do niego przypisaną (`AssignedTo`). Wykorzystamy do tego poniższy kod klasy `DapperExtensions`. Dodaj go do folderu `BusinessLogic` i użyj metodę rozszerzającą `FixDapperMappings` na obiekcie klasy `TaskItem` we wskazanych metodach. - -
-Pokaż kod DapperExtensions - -Niniejsze rozwiązanie używa tzw. refleksji. Refleksja w C# to zdolność programu do analizy własnej struktury, informacji o typach i manipulowania nimi w trakcie działania programu. Pozwala na dynamiczne badanie, dostęp i modyfikację typów, metod, właściwości, pól itp. w czasie wykonania. Refleksja jest zaawansowanym i potężnym narzędziem, ale należy z nią obchodzić się ostrożnie, ponieważ może prowadzić do kodu trudnego do zrozumienia i utrzymania. Nie będziemy omawiali na tym kursie szczegółowego działania refleksji. - -```csharp -using System.Reflection; - -namespace TaskManager.BusinessLogic -{ - public static class DapperExtensions - { - public static TaskItem FixDapperMapping(this TaskItem taskItem, User createdBy, User assignedTo) - { - SetValueToObject(taskItem, "_createdBy", createdBy); - SetValueToObject(taskItem, "_assignedTo", assignedTo); - return taskItem; - } - - private static void SetValueToObject(object obj, string fieldName, object value) - { - var type = obj.GetType(); - var field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); - field.SetValue(obj, value); - } - } -} -``` -
- -#### **Wersja podstawowa:** - -- `GetAllUsersAsync`: Użyj odpowiedniej metody Dappera do pobrania wszystkich użytkowników (bez zadań). -- `GetUserByIdAsync`: Użyj odpowiedniej metody Dappera do pobrania jednego użytkownika (bez zadań). -- `CreateTaskItemAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu, który utworzy zadanie (przekazując opis, użytkownika tworzącego, datę utworzenia, satus, i datę ważności zadania) i zwróci na końcu ID nowego zadania (nadane przez bazę danych). Następnie pobierz na podstawie tego ID obiekt z bazy danych i zwróć go (możesz wykorzystać do tego metodę `GetTaskItemByIdAsync`). Do pobrania nadanego ID możesz użyć skryptu `SELECT SCOPE_IDENTITY();` -- `UpdateTaskItemAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu modyfikacji zadania (przekazując status, datę startu, datę zakończenia zadania, osobę przypisaną do zadania) na podstawie podanego ID. Zwróć informację z metody o tym, czy udało się zaktualizować zadanie. -- `DeleteTaskItemAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu, który usunie jedno zadanie na podstawie podanego ID. Zwróć informację z metody o tym, czy udało się usunąć zadanie. -- `GetTaskItemByIdAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu, który pobierze pełny obiekt zadania (tzn. wraz z przypisanymi do niego użytkownikami jako twórcy i wykonawcy zadania). Zwróć obiekt zadania. W tym miejscu musimy wykorzystać nasze obejście `FixDapperMapping`. Sprawdź podpowiedź znajdującą się poniżej. -- `GetAllTaskItemsAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu, który pobierze wszystkie pełne obiekty zadania (tzn. wraz z przypisanymi do niego użytkownikami jako twórcy i wykonawcy zadania). Zwróć tablicę zadań. W tym miejscu musimy wykorzystać nasze obejście `FixDapperMapping`. Sprawdź podpowiedź znajdującą się poniżej. -- `GetTaskItemsByStatusAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu, który pobierze wszystkie pełne obiekty zadania (tzn. wraz z przypisanymi do niego użytkownikami jako twórcy i wykonawcy zadania) na podstawie podanego statusu zadania. Zwróć tablicę zadań. W tym miejscu musimy wykorzystać nasze obejście `FixDapperMapping`. Sprawdź podpowiedź znajdującą się poniżej. -- `GetTaskItemsByDescriptionAsync`: Użyj odpowiedniej metody Dappera do wywołania skryptu, który pobierze wszystkie pełne obiekty zadania (tzn. wraz z przypisanymi do niego użytkownikami jako twórcy i wykonawcy zadania) na podstawie podanej frazy występującej w opisie zadania (użyj `%FRAZA%`). Zwróć tablicę zadań. W tym miejscu musimy wykorzystać nasze obejście `FixDapperMapping`. Sprawdź podpowiedź znajdującą się poniżej. - -
-Podpowiedź do użycia FixDapperMapping - -Przykład pobrania jednego elementu, ale przy użyciu `QueryAsync` (w przypadku pobierania relacji JOIN dla pojedynczego rekordu, musimy użyć `QueryAsync`): -```csharp -var tasks = await connection.QueryAsync( - sql, - (task, createdBy, assignedTo) => - { - return task.FixDapperMapping(createdBy, assignedTo); - }, - new {TaskId = taskItemId}); -return tasks.FirstOrDefault(); -``` - -Przykład pobierania wielu elementów bez filtrowania: -```csharp -var tasks = await connection.QueryAsync( - sql, - (task, createdBy, assignedTo) => - { - return task.FixDapperMapping(createdBy, assignedTo); - }); -return tasks.ToArray(); -``` - -Przykład pobrania wielu elementów z filtrowaniem: -```csharp -var tasks = await connection.QueryAsync( - sql, - (task, createdBy, assignedTo) => - { - return task.FixDapperMapping(createdBy, assignedTo); - }, - new {TaskStatus = status}); -return tasks.ToArray(); -``` -
- -#### **Wersja rozszerzona (dla chętnych):** -- `GetAllUsersAsync`: Zmodyfikuj metodę tak, aby pobrała pełne obiekty użytkownika wraz z przypisanymi do niego zadaniami. -- `GetUserByIdAsync`: Zmodyfikuj metodę tak, aby pobrała pełny obiekt użytkownika wraz z przypisanymi do niego zadaniami na podstawie podanego ID użytkownika. - ---- - -### **12. Dodaj nowe funkcjonalności do aplikacji konsolowej.** - -Rozszerz opcje aplikacji konsolowej o: -- wyświetlanie użytkowników, -- przypisywanie zadania do użytkownika. - -Pamiętaj, aby wyświetlić nowe opcje w menu w interfejsie użytkownika. - -#### **Rozszerz wyświetlanie szczegółów o zadaniu:** -- Wyświetl dodatkowe informacje dla szczegółów zadania o twórcę zadania i osobę przypisaną do zadania. - -#### **Wyświetlanie listy użytkowników:** -- Rozszerz klasę `TaskManagerService` o metodę `GetAllUsersAsync`. Wykorzystaj istniejącą metodę repozytorium `GetAllUsersAsync`. -- W konsoli wywołaj nową metodę serwisu i wyświetl zwrócone dane. - -#### **Przypisywanie użytkownika do zadania:** -- Rozszerz klasę `TaskManagerService` o metodę `AssignToAsync(taskId, userId)`, która zwróci informację czy udało się ustawić wykonawcę zadania. Niech metoda pobierze aktualną wersję zadania oraz użytkownika, następnie w modelu biznesowym zadania wywołaj metodę `AssignTo`, a na koniec zaktualizuj model zadania w bazie danych. ID użytkownika jest opcjonalne i może przyjąć wartość `null`, wówczas należy odsunąć użytkownika od zadania (i przekazać `null` do metody `AssignTo`). Metoda `AssignToAsync` powinna używać już istniejące metody serwisu i repozytorium, aby nie powielać kodu. -- W konsoli wywołaj nową metodę serwisu i wyświetl stosowny komunikat. - ---- - -### **13. Dodatkowe testy jednostkowe.** - -Dopisz testy jednostkowe sprawdzające `TaskItem` oraz `TaskManagerService`. - -#### **`TaskItemTests`:** -- Dopisz dwa scenariusze testowe sprawdzający działanie metody `AssignTo`, w przypadku gdy: - - podamy obiekt użytkownika, - - podamy wartość `null`. - -#### **`TaskManagerServiceTests`:** -- Dopisz cztery scenariusze testowe sprawdzające działanie metody `AssignToAsync`, w przypadku gdy: - - podamy właściwe ID zadania i użytkownika (przypisze zadanie do użytkownika), - - podamy właściwe ID zadania i pustego użytkownika (ustawi `null` przypisanemu użytkownikowi), - - podamy właściwe ID zadania, ale nie właściwe ID użytkownika (nie ustawi użytkownika), - - podamy niewłaściwe ID zadania (nie ustawi użytkownika). - ---- - -### **14. GRATULACJE! Właśnie jesteś na końcu warsztatów.** - -Gratulujemy i dziękujemy za aktywny udział w warsztatach. Twoje zaangażowanie i chęć do nauki są dla nas inspiracją. Życzymy powodzenia w dalszej części kursu! - ---- - -### **Aplikację możesz dowolnie rozszerzyć o dodatkowe funkcjonalności.** - -Pamiętaj, że ogranicza Cię tylko Twoja wyobraźnia. Możesz dalej rozwijać tę aplikację i rozszerzać ją o dodatkowe funkcjonalności, np.: -- wyświetlanie Twoich zadań, -- wyświetlanie zadań pogrupowanych po użytkownikach wykonujących je, -- wyświetlanie nieprzypisanych zadań, -- i wiele, wiele innych... - - ---- ---- ---- ---- - -## Podsumowanie warsztatów TaskManager z Dapper - -Po intensywnej pracy pełnej nauki i programowania, nadszedł czas na podsumowanie warsztatów dotyczących tworzenia aplikacji TaskManager w języku C#. - -### Główne punkty warsztatu - -1. **Podział aplikacji na warstwy**: Podzieliliśmy kod aplikacji na mniejsze bloczki: -- modele biznesowe (warstwa domeny), -- repozytorium (warstwa dostępu do danych), -- serwis (warstwa aplikacji), -- konsola (warstwa prezentacji). - -2. **Rozbudowanie modeli**: Rozbudowaliśmy model zadania o nowe cechy i akcje. Utworzliśmy nowy model użytkownika. - -3. **Utworzenie bazy danych**: Utworzyliśmy bazę danych `TaskManager` z tabelami zadań i użytkowników. - -4. **Połączenie z bazą danych przy użyciu Dapper**: Zainstalowaliśmy i skonfigurowaliśmy paczkę NuGet Dapper. Utorzyliśmy interfejs i repozytorium, za pomocą którego łączymy się z bazą danych. - -5. **Rozbudowanie testów**: Zmodyfikowaliśmy testy o wykorzystanie mock-a repozytorium. Dopisaliśmy nowe scenariusze testowe. - -6. **Rozbudowanie aplikacji konsolowej**: Zmodyfikowaliśmy aplikację konsolową i wprowadziliśmy programowanie asynchroniczne. Dodaliśmy nowe funkcje do aplikacji. - -### Dalsze kroki - -Zachęcamy do rozwijania aplikacji TaskManager. Możliwe są takie rozszerzenia jak: dodawanie priorytetów dla zadań, dodanie śledzenia historii zmian w zadaniu, dodawanie komentarzy do zadań, tworzenie interfejsu programistycznego WebAPI i interfejsu graficznego dla użytkownika w formie strony WWW. - -### Gratulacje! - -Życzymy powodzenia w dalszej części kursu! +**Repozytorium z ćwiczeniami zostanie usunięte 2 tygodnie po zakończeniu kursu. Spowoduje to też usunięcie wszystkich forków, które są zrobione z tego repozytorium.** From 4d725a1e748ac76f64e8a7b673052187a7da63d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Wasilewski-CL?= Date: Thu, 21 Mar 2024 06:26:59 +0100 Subject: [PATCH 3/7] FIX Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1204b17..7d660bd 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Jeżeli chcesz dodać wszystkie zmienione pliki użyj `git add .` Pamiętaj że kropka na końcu jest ważna! Następnie skommituj zmiany komendą `git commit -m "nazwa_commita"` 5. Wypchnij zmiany do swojego brancha w repozytorium na GitHubie. Użyj do tego komendy `git push origin feature/` (podstaw za `X` Twoją nazwę brancha). -6. Stwórz [*pull request*](https://help.github.com/articles/creating-a-pull-request) z Twojego brancha do brancha `gitflow`, gdy skończysz wszystkie zadania. +6. Stwórz [*pull request*](https://help.github.com/articles/creating-a-pull-request) z Twojego brancha do brancha `gitflow/NUMER_KURSU`, gdy skończysz wszystkie zadania. Poszczególne zadania rozwiązuj w odpowiednich plikach. From ef9a66ca46ac95bc00de1dab05e371cad8630988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Wasilewski-CL?= Date: Thu, 21 Mar 2024 06:30:22 +0100 Subject: [PATCH 4/7] FIX Readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7d660bd..997ed3e 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ Przed przystąpieniem do rozwiązywania zadań przeczytaj poniższe wskazówki. 1. Sklonuj oryginalne repozytorium na swój komputer (nie klonuj forka). Użyj do tego komendy `git clone adres_repozytorium` Adres możesz znaleźć na stronie repozytorium po naciśnięciu w guzik "Clone or download". -2. Przełącz się na branch `gitflow/NUMER_KURSU` tego repozytorium (w miejsce NUMER_KURSU podstaw rzeczywistą sygnaturę kursu). +2. Przełącz się na branch `gitflow-NUMER_KURSU` tego repozytorium (w miejsce NUMER_KURSU podstaw rzeczywistą sygnaturę kursu). 3. Utwórz właśny unikalny branch `feature/X`, wstawiając za `X` unikalną nazwę, np. numer kursu i Twoje inicjały z liczbą. 4. Rozwiąż zadania i skomituj zmiany do swojego brancha z punktu 3. Użyj do tego komend `git add nazwa_pliku`. Jeżeli chcesz dodać wszystkie zmienione pliki użyj `git add .` Pamiętaj że kropka na końcu jest ważna! Następnie skommituj zmiany komendą `git commit -m "nazwa_commita"` 5. Wypchnij zmiany do swojego brancha w repozytorium na GitHubie. Użyj do tego komendy `git push origin feature/` (podstaw za `X` Twoją nazwę brancha). -6. Stwórz [*pull request*](https://help.github.com/articles/creating-a-pull-request) z Twojego brancha do brancha `gitflow/NUMER_KURSU`, gdy skończysz wszystkie zadania. +6. Stwórz [*pull request*](https://help.github.com/articles/creating-a-pull-request) z Twojego brancha do brancha `gitflow-NUMER_KURSU`, gdy skończysz wszystkie zadania. Poszczególne zadania rozwiązuj w odpowiednich plikach. From d5b94dec4f1d8e3f97b17ee49ce3d5bd5c4f312a Mon Sep 17 00:00:00 2001 From: TomekWili Date: Sun, 24 Mar 2024 10:17:07 +0100 Subject: [PATCH 5/7] DEL Assign --- TaskManager/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TaskManager/Program.cs b/TaskManager/Program.cs index f8dc406..2c66c62 100644 --- a/TaskManager/Program.cs +++ b/TaskManager/Program.cs @@ -57,9 +57,9 @@ public static async Task Main() case "7": await UpdateTaskStatusAsync(); break; - case "8": - await AssignTaskAsync(); - break; + //case "8": + // await AssignTaskAsync(); + // break; } Console.WriteLine(""); } while (command != "9"); From 05adfc73e4ec92da886c7418bcfbf276ceb9a709 Mon Sep 17 00:00:00 2001 From: TomekWili Date: Sun, 31 Mar 2024 19:39:34 +0200 Subject: [PATCH 6/7] =?UTF-8?q?ADD=20Logowanie=20zdarze=C5=84=20w=20system?= =?UTF-8?q?ie=20z=20Serilog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TaskManager/BusinessLogic/IRepository.cs | 6 ++ TaskManager/BusinessLogic/LogCountByDayDto.cs | 14 +++++ TaskManager/BusinessLogic/Repository.cs | 51 ++++++++++++++++ TaskManager/BusinessLogic/SeriLog.cs | 19 ++++++ .../BusinessLogic/SerilogController.cs | 58 +++++++++++++++++++ TaskManager/Program.cs | 19 ++++-- TaskManager/TaskManager.csproj | 3 + 7 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 TaskManager/BusinessLogic/LogCountByDayDto.cs create mode 100644 TaskManager/BusinessLogic/SeriLog.cs create mode 100644 TaskManager/BusinessLogic/SerilogController.cs diff --git a/TaskManager/BusinessLogic/IRepository.cs b/TaskManager/BusinessLogic/IRepository.cs index 6cc9ef0..2cd4860 100644 --- a/TaskManager/BusinessLogic/IRepository.cs +++ b/TaskManager/BusinessLogic/IRepository.cs @@ -11,5 +11,11 @@ public interface IRepository Task GetAllTaskItemsAsync(); Task GetTaskItemsByStatusAsync(TaskItemStatus status); Task GetTaskItemsByDescriptionAsync(string description); + Task> GetAllAsync(); + Task GetByIdAsync(int id); + Task> GetLogCountByDayAsync(); + Task> GetByLogLevelAsync(string logLevel); + Task> GetByMessageAsync(string searchTerm); + } } \ No newline at end of file diff --git a/TaskManager/BusinessLogic/LogCountByDayDto.cs b/TaskManager/BusinessLogic/LogCountByDayDto.cs new file mode 100644 index 0000000..bf4c2ce --- /dev/null +++ b/TaskManager/BusinessLogic/LogCountByDayDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TaskManager.BusinessLogic +{ + public class LogCountByDayDto + { + public DateTime Day { get; set; } + public int Count { get; set; } + } +} diff --git a/TaskManager/BusinessLogic/Repository.cs b/TaskManager/BusinessLogic/Repository.cs index 3eff97d..caa48ec 100644 --- a/TaskManager/BusinessLogic/Repository.cs +++ b/TaskManager/BusinessLogic/Repository.cs @@ -161,5 +161,56 @@ public async Task GetTaskItemsByDescriptionAsync(string description) return tasks.ToArray(); } } + public async Task> GetAllAsync() + { + using (var connection = new SqlConnection(_connectionString)) + { + var sql = "SELECT * FROM SeriLogs"; + return await connection.QueryAsync(sql); + } + } + public async Task GetByIdAsync(int id) + { + using (var connection = new SqlConnection(_connectionString)) + { + var sql = "SELECT * FROM SeriLogs WHERE Id = @Id"; + return (await connection.QueryAsync(sql, new { Id = id })).FirstOrDefault(); + } + } + public async Task> GetLogCountByDayAsync() + { + using (var connection = new SqlConnection(_connectionString)) + { + var sql = @" + SELECT + CAST(TimeStamp AS DATE) AS Day, + COUNT(*) AS Count + FROM SeriLogs + GROUP BY CAST(TimeStamp AS DATE) + ORDER BY Day"; + var results = await connection.QueryAsync(sql); + return results; + } + } + public async Task> GetByLogLevelAsync(string logLevel) + { + using (var connection = new SqlConnection(_connectionString)) + { + var sql = @" + SELECT * + FROM SeriLogs + WHERE Level = @Level"; + var results = await connection.QueryAsync(sql, new { Level = logLevel }); + return results; + } + } + public async Task> GetByMessageAsync(string searchTerm) + { + using (var connection = new SqlConnection(_connectionString)) + { + var sql = "SELECT * FROM SeriLogs WHERE Message LIKE @SearchTerm"; + return await connection.QueryAsync(sql, new { SearchTerm = $"%{searchTerm}%" }); + } + } } } \ No newline at end of file diff --git a/TaskManager/BusinessLogic/SeriLog.cs b/TaskManager/BusinessLogic/SeriLog.cs new file mode 100644 index 0000000..6feb390 --- /dev/null +++ b/TaskManager/BusinessLogic/SeriLog.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TaskManager.BusinessLogic +{ + public class SeriLog + { + public int Id { get; set; } + public string Message { get; set; } + public string MessageTemplate { get; set; } + public string Level { get; set; } + public DateTime TimeStamp { get; set; } + public string Exception { get; set; } + public string Properties { get; set; } + } +} diff --git a/TaskManager/BusinessLogic/SerilogController.cs b/TaskManager/BusinessLogic/SerilogController.cs new file mode 100644 index 0000000..c510afd --- /dev/null +++ b/TaskManager/BusinessLogic/SerilogController.cs @@ -0,0 +1,58 @@ +using TaskManager.BusinessLogic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Runtime.InteropServices; + +namespace TaskManager.BusinessLogic +{ + [Route("api/[controller]")] + [ApiController] + public class SeriLogController : ControllerBase + { + private IRepository _repository; + + public SeriLogController(IRepository logRepository) + { + _repository = logRepository; + } + + [HttpGet] + public async Task GetAll() + { + var logs = await _repository.GetAllAsync(); + return Ok(logs); + } + + [HttpGet("{id}")] + public async Task GetById(int id) + { + var log = await _repository.GetByIdAsync(id); + if (log == null) + { + return NotFound(); + } + return Ok(log); + } + + [HttpGet("count-by-day")] + public async Task GetLogCountByDay() + { + var logs = await _repository.GetLogCountByDayAsync(); + return Ok(logs); + } + + [HttpGet("get-by-logLevel")] + public async Task GetByLogLevel(string logLevel) + { + var logs = await _repository.GetByLogLevelAsync(logLevel); + return Ok(logs); + } + + [HttpGet("get-by-message")] + public async Task GetByMessage(string message) + { + var logs = await _repository.GetByMessageAsync(message); + return Ok(logs); + } + } +} diff --git a/TaskManager/Program.cs b/TaskManager/Program.cs index 2c66c62..c81d27a 100644 --- a/TaskManager/Program.cs +++ b/TaskManager/Program.cs @@ -2,10 +2,20 @@ using System.Text; using Dapper; using TaskManager.BusinessLogic; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Serilog; -namespace TaskManager -{ - public class Program + var builder = WebApplication.CreateBuilder(args); + + builder.Host.UseSerilog((hostingContext, loggerConfiguration) => + loggerConfiguration.ReadFrom.Configuration(hostingContext.Configuration)); + + builder.Services.AddScoped(); + + var app = builder.Build(); + + public partial class Program { private const string ConnectionString = "Server=localhost,1433;Initial Catalog=TaskManager;User ID=sa;Password=P@ssw0rd;Encrypt=True;TrustServerCertificate=True;Connection Timeout=30;"; @@ -273,5 +283,4 @@ private static async Task TestDbAsync() return result; } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/TaskManager/TaskManager.csproj b/TaskManager/TaskManager.csproj index eb91a06..f6c51dc 100644 --- a/TaskManager/TaskManager.csproj +++ b/TaskManager/TaskManager.csproj @@ -9,6 +9,9 @@ + + + From 6fdd6bd130d9407d5722ca85b4817b382894226b Mon Sep 17 00:00:00 2001 From: TomekWili Date: Thu, 4 Apr 2024 00:07:55 +0200 Subject: [PATCH 7/7] BUG repair --- TaskManager/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TaskManager/Program.cs b/TaskManager/Program.cs index c81d27a..111a93e 100644 --- a/TaskManager/Program.cs +++ b/TaskManager/Program.cs @@ -11,7 +11,7 @@ builder.Host.UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration.ReadFrom.Configuration(hostingContext.Configuration)); - builder.Services.AddScoped(); + builder.Services.AddScoped(); var app = builder.Build();