.NET Core WebAPI - serwisy, repozytoria, automapery, DTO, Dapper, EF

6

Witam.
Dręczy mnie to trochę i chciałbym znać wasze zdanie. W pewnym poście dostałem po tyłku za programowanie i owszem, należało mi się, ale są rzeczy, których nie rozumiem i mam przeczucie, że mnie nie dotyczą. A że z natury jestem człowiekiem, który nie trzyma się zasad jak nie mają sensu to później dostaje bo łbie za pisanie głupot...

  1. Czy konieczny jest podział solucji na projekty, gdzie każdy projekt za coś odpowiada. Dlaczego nie można tego rozdzielić folderami w jednym projekcie?
  2. Czym się różnią serwisy od repozytoriów i dlaczego to nazewnictwo jest takie ważne?
  3. Co powinien zawierać kontroler? Dlaczego akcja kontrolera nie może zawierać logiki biznesowej?
  4. Gdzie powinno być połączenie do bazy jeśli nie korzystam z EF, a na przykład z Dappera?
  5. Obsługa błędów, wyjątków, co wysyłać do klienta, co obsłużyć po stronie API?
  6. Czy asynchroniczność jest konieczna w przypadku WebAPI?
  7. W jakich przypadkach przydaje się automapper?
  8. Do czego "używa się" DTO?

Pytania dodatkowe

  1. Czy to nie jest strata czasu?
  2. Czy jeśli jestem jedynym programistą w firmie to te wszystkie zasady mnie "obowiązują" lub powinny "obowiązywać"?
5

Cześć,

Z mojej perspektywy (bardziej doświadczeni pewnie mnie poprawią):

Ad. 1. Nie jest konieczny, jest wskazany to poniekąd podążanie za zasadami SOLID w większym uogólnieniu. Każdy projekt może realizować różne zadania i możesz deploywać pojedyncze projekty zamiast całości.
Ad. 2. Repozytoria to dodatkowa warstwa, która po pierwsze ułatwia testowanie, a po drugie w przypadku rozbudowanych projektów i np. widoku który potrzebuje danych z dwóch serwisów masz możliwość połączenia tego właśnie w repozytorium. (Tutaj może lepiej jakby ktoś bardziej doświadczony to wyjaśnił).
Ad. 3. Nie może to duże słowo. Nie powinna, od tego masz warstwę logiki biznesowej, aby tam ją umieścić. Znowu - ułatwia to testowanie i wprowadza pewien porządek w projekcie. Kontrolery są po to, aby zwracać konkretne rzeczy (np. model) do widoku.
Ad. 4. W tym samym miejscu co w przypadku EF. To i to ostatecznie jest ORMem, wiec dlaczego chciałbyś to umieszczać w innych miejscach? Możesz to w przypadku ASP Core dodać do konfiguracji.
Ad. 5. Co dokładnie masz na myśli? Do użytkownika wysyłasz komunikat zwrotny, że coś poszło nie tak. Cały wyjątek obsługujesz po stronie API np. przekierowując użytkownika do innego widoku.
Ad. 6. Konieczna to znowu duże słowo. Przyspiesza działanie aplikacji, więc dlaczego z tego nie korzystać?
Ad. 7. Automapper przydaje się kiedy aplikacja sama nie potrafi sobie przetłumaczyć obiektów EF na Twoje modele.
Ad. 8. To kontener na dane. Pobierając całą encję z bazy możesz sobie w obiekcie DTO usunąć property których nie potrzebujesz lub nie chcesz pokazywać (np. hasła) i używać obiektu DTO.

Ad. 1. Nie, nie jest. To są dobre zasady programowania których może teraz nie rozumiesz i nie doceniasz, ale z czasem (i nabytym doświadczeniem) zobaczysz, że to wszystko ma jakiś sens.
Ad. 2. Parafrazując tekst który kiedyś przeczytałem - nie ważne jak zły kod napisałeś jeśli napisałeś go sam to będziesz wiedział co robi. Teraz wyobraź sobie, że pracujesz z kimś i ktoś ma zmienić coś po Tobie. Nie trzymając się dobrych praktyk byłaby to masakra. Nawet jeśli sam będzie pracował w jakimś projekcie i zrobisz go w niepoprawny sposób, ale pomimo to będzie działał to wyobraź sobie sytuację, że za pół roku siadasz znowu do tego samego projektu i masz coś zmienić, a okazuje się, że projekt to takie spaghetti code, że szybciej i łatwiej będzie napisać ten projekt od zera niż zmieniać to co jest.

Pozdrawiam

0
  1. Podział na projekty wiele ułatwia. Przykładowo dwie różne warstwy mogą być siebie nieświadome i nie kusi wtedy żeby je przypadkiem powiązać, żeby coś szybciej zrobić.
  2. Nie wiem co dokładnie autor ma na myśli. Powiedziałbym ze serwisy to warstwa biznesowa a repozytoria to warstwa danych.
  3. Ja to sobie tłumaczę tak, że kontroler nie jest częścią aplikacji. On tylko spina aplikację ze światem. Kontroler służy do tego, żeby przyjąć dane i zwrócić dane. Nie interesuje go jakie to są dane. Jeżeli cała logika biznesowa by była w kontrolerach to miałaby mocne zależności do protokołu http, a przecież teoretycznie mogłbyś chcieć jakiś kawałek aplikacji wystawić gdzieś indziej, zdala od WebApi, MVC czy czegokolwiek takiego.
  4. Połączenie do bazy tworzy warstwa danych oczywiście. Logika biznesowa nic o bazie danych nie wie.
  5. Rozwiń pytanie.
  6. Nie jest konieczna, ale poprawia jej skalowalność jeżeli spodziewasz się regularnego przyrostu ilości zapytań.
  7. Automapper przydaje się jeżeli często tworzysz obiekty dwóch róznych typów o podobnej strukturze.
  8. DTO to obiekt, którego zadaniem jest przenosić dane między warstwami w aplikacji. Każda warstwa ma swoje typy, nie powinieneś na przykład w kontrolerze operować na encjach EF - od tego są DTO.

1, 2. Nie jest stratą czasu. Dobrze napisany kod dużo łatwiej poźniej rozwijać. Poza tym nie masz pewności, że zawsze będziesz tam sam.

1

Nie wiem, czy wprowadzę coś nowego, ale tak:

  1. Czy konieczny jest podział solucji na projekty, gdzie każdy projekt za coś odpowiada. Dlaczego nie można tego rozdzielić folderami w jednym projekcie?

Pewnie, że można. Jednak nie jest to wskazane. Wyobraź sobie, że masz napisać aplikację desktopową do przerabiania zdjęć. Np. dajesz zdjęcie, a ona przerabia Ci każdy czerwony piksel na czarny. Napisałeś ją w WinForms. No i super, wszystko działa.
Lecz teraz przychodzi moment, kiedy ktoś mówi - ta aplikacja jest zajebista, wrzuć ją do Microsoft Store! I to niby nie wymaga niczego, bo wystarczy ją wrzucić... ale chyba nie wrzucisz. Bo to nie jest UWP. A ponoć tylko UWP (i ostatnio słyszałem, że WPF) można wrzucać do MS Store. Więc co robisz? Piszesz tą samą aplikację od nowa?

Najbardziej poprawna droga jest taka, żeby oddzielić GUI od zadania. Czyli powstają Ci w ten sposób przynajmniej dwa projekty - GUI i dll z faktycznymi operacjami do zmiany obrazów. I co się dzieje teraz? Możesz stworzyć trzeci projekt - UWP - tylko GUI, które będzie korzystało z Twojej dllki. Teraz niech ktoś podejdzie i powie - super, przerzuć to do Weba i na telefony. Masz właściwie problem z głowy, bo już masz wszytko gotowe. Musisz tylko naklepać nowe GUI. To taki najprostszy przykład, dlaczego warto używać kilku projektów.

  1. Czym się różnią serwisy od repozytoriów i dlaczego to nazewnictwo jest takie ważne?

Repozytorium to warstwa danych. Odpowiada za odczyt/zapis danych. Serwis odpowiada za obróbkę tych danych, np:

Repozytorium:

public void SaveUser(User user);
public User LoadUser(int userId);

Serwis:

public User CreateNewUser(string name, string pass)
{
  if(string.IsNullOrWhitespace(name))
    throw new ArgumentException("Nie podano nazwy użytkownika!");

  if(!PasswordHelper.PassIsValid(pass))
    throw new ArgumentException("Niepoprawne hasło!");

  User createdUser = new User(name, pass);
  repo.SaveUser(createdUser);

  return createdUser;
}

Co tu widać:

  • repozytorium jest odpowiedzialne tylko za przekazywanie danych
  • serwis jest odpowiedzialny za logikę biznesową - może wykorzystywać repozytorium, ale tak naprawdę ma w głębokim poważaniu, gdzie dane są zapisywane. Czy to będzie MSSQL, MySql, SQLite, a może plik tekstowy. To wie tylko repozytorium. Dzięki temu, rozdzielasz sobie zapis danych od działania na tych danych. I dobrze. Dzięki repozytorium możesz zapisywać dane w dowolnym formacie, a tak naprawdę logika biznesowa w ogóle się nie zmienia. Ułatwia to też testowanie. Gdy testujesz metodę CreateNewUser, to nie testujesz zapisu danych, tylko odpowiednią logikę biznesową - np. czy się wywali, jak nie będzie podanego hasła. Repozytorium mockujesz.
  1. Co powinien zawierać kontroler? Dlaczego akcja kontrolera nie może zawierać logiki biznesowej?
    Widziałem sporo projektów, gdzie zawierała. Przede wszystkim mnóstwo przykładowych projektów dla różnych frameworków właśnie tak jest skonstruowana, że kontroler zawiera logikę. Utrudnia to jednak testowanie. Dlatego zdecydowanie lepszy kod jest taki, gdzie kontroler korzysta z serwisu, a sam robi zupełne minimum tak, żeby go nie trzeba było testować.
  1. Gdzie powinno być połączenie do bazy jeśli nie korzystam z EF, a na przykład z Dappera?
    Tu wchodzimy w kolejną warstwę. Warstwę danych. Czyli osobny projekt :)
    Tutaj połączenie z bazą może być na poziomie repozytorium. Albo jeszcze niżej.
  1. Obsługa błędów, wyjątków, co wysyłać do klienta, co obsłużyć po stronie API?
    Generalnie API nie powinno się nigdy wywalić :)
    Do klienta powinieneś wysłać informację o statusie operacji - zakończono sukcesem / zakończono błędem.
    Jeśli jest błąd, powinieneś mu wysłać ten błąd (chociażby jako BadRequest, jeśli boisz się że błąd może posłużyć shackowaniu API). Ale miło jest jak klient otrzyma jakąś wskazówkę, dlaczego błąd wystąpił: "Za krótkie hasło / niepoprawna nazwa użytkownika / Błąd bazy danych". Nie powinieneś jednak wysyłać pewnych problemów, np: "Invalid syntax near: "INSERT into tabela....", bo to może zagrać na Twoją niekorzyść.
  1. Czy asynchroniczność jest konieczna w przypadku WebAPI?
    Nie
  1. W jakich przypadkach przydaje się automapper?
    Wg mnie w prostych, standardowych, nieproblematycznych.
  1. Do czego "używa się" DTO?
    To jest prosta klasa do przesyłania danych. Np masz model usera:
public class User
{
    public int Id {get;set;}
    public string Name {get;set;}
    public string UserName {get;set;}
    public string EMail {get;set;}
   //i mnóstwo innych pól, w tym jakieś krytyczne
}

I teraz musisz przesłać dane z API do klienta albo na odwrót. Nie musisz pakować wszystkich danych. Wystarczy, że zrobisz sobie jakąś lekką klasę:

public class UserDto
{
    public int Id {get;set;}
    public string UserName {get;set;}
}

i tyle. Bez żadnej logiki bez niczego więcej. Bo musisz przekazywać akurat tylko Id i nazwę użytkownika. Nic więcej nie jest Ci potrzebne (takie założenie). Do tego jest DTO.

Pytania dodatkowe

  1. Czy to nie jest strata czasu?

Na początku tak. Bo projekt powstaje dłużej. Jednak późniejszy jego rozwój i utrzymanie zajmują dużo mniej czasu niż monolitycznego odpowiednika. Co więcej, dobrze rozdzielony projekt łatwo testować automatycznie. A więc masz produkt dużo bardziej stabliny.

  1. Czy jeśli jestem jedynym programistą w firmie to te wszystkie zasady mnie "obowiązują" lub powinny "obowiązywać"?

Jak najbardziej.

4

To są bardzo dobre pytania, bo nie wszystkie "dobre praktyki" są dobre w każdym kontekście, już nie wspominając że określenie że kod jest "dobrze napisany" jest bardzo subiektywne.
Także bardzo dobrze, że zadajesz, dobre pytania a nie podążasz na ślepo za cargo kultem dobrych praktyk, tylko próbujesz dociec dlaczego daną rzecz się stosuje i zrozumieć jaki problem ona rozwiązuje. Także zakończę ten drobny offtop mottem ze świata reacta: "Rethinking best practices".

0

Ok, argumentacja jest i dociera do mnie. Kilka osób wspomniało o testach. Moim zdaniem kolejna strata czasu. Rozumiem, że "dobre praktyki" robi się po to, aby się aplikację dobrze testowało?

TESTOWANIE

Ad. 2. Repozytoria to dodatkowa warstwa, która po pierwsze ułatwia testowanie,

Co więcej, dobrze rozdzielony projekt łatwo testować automatycznie. A więc masz produkt dużo bardziej stabliny.
...
Przede wszystkim mnóstwo przykładowych projektów dla różnych frameworków właśnie tak jest skonstruowana, że kontroler zawiera logikę. Utrudnia to jednak testowanie


DZIWNE ODPOWIEDZI
Albo ja zbyt logicznie podchodzę do tego co tutaj piszecie.
@Fuffu Chcesz powiedzieć, że używasz dobrych praktyk, aby cię "nie kusiło", żeby rozwiązać coś inaczej, szybciej, niezgodnie z dobrymi praktykami? 🤔🙈

Podział na projekty wiele ułatwia. Przykładowo dwie różne warstwy mogą być siebie nieświadome i nie kusi wtedy żeby je przypadkiem powiązać, żeby coś szybciej zrobić.


DZIELENIE NA PROJEKTY
@Krzysztof Pe Jeśli wprowadzisz coś nowego w jednym z projektów to i tak musisz z tego skorzystać w kontrolerze, aby w ogóle wyszło to "na świat". To jest dobre w kwestii poprawek. Wtedy podmieniasz jedną DLL-kę i działa.

Każdy projekt może realizować różne zadania i możesz deploywać pojedyncze projekty zamiast całości.

PODSUMOWANIE:
Spróbuje to ogarnąć jakoś w sumie, odpowiedzi są bardzo podobne. Skoro repozytorium zapisuje i odczytuje dane to dlaczego nie mogę ich od razu "obrobić" w repozytorium i zapisać? Chcecie mi powiedzieć, że ze względu na "dobre praktyki" powinienem stworzyć interfejs (serwis), który mi te dane przerobi, tylko po to, aby repozytorium mogło zapisać w takiej formie w jakiej powinno być zapisane?

PRZYKŁAD:

public bool CancelAgreement(int agreementId)
{
    var agreement = _dbContext.Agreements.Find(agreementId);
    
    //Te dwie linie poniżej powinny być w serwisie? Po co?
    agreement.Canceled = true;
    agreement.CancelDate = DateTime.Now;

    _dbContext.Agreements.Update(agreement);
    _dbContext.SaveChanges();
}

Może kiedyś nadejdzie dzień, w którym zrozumiem wasze "podejście". Niektóre argumenty są dobre, np.: DTO, szczególnie przy Dapperze, gdzie można sobie stworzyć taki obiekt, który jest wynikiem dość potężnego join`owania.

**Wniosek? **
Uważam, że więcej czasu spędza się na generowaniu tych wszystkich interfejsów do serwisów i repozytoriów niż na faktycznym programowaniu. Rozumiem jakby omówione kwestie miały wpływ na pracę aplikacji, jej szybkość, wydajność, ale to jest tylko, albo też aż, jakiś sposób na organizację projektu. Jeśli więcej osób pracuje nad jednym projektem to ok, każdy piszę tak samo i każdym wie gdzie i czego się spodziewać.

PS.
Ja pracuje w bardzo małej firmie (5 osób, razem ze mną). Programowanie to tylko dodatek do usług jakie oferujemy. Ostatnio zrobiło się zapotrzebowanie na webowe aplikacje, ponieważ ludzie chcą mieć dostęp zewsząd. Przerzucając się z WinForms, to dla mnie to duża nowość. Mam już kilka projektów na swoim koncie, ale żaden z nich nie jest zrobiony w sposób w jaki tutaj opisujecie i (co dziwne) wszystko działa.

0

@AdamWox ja kieruje się trochę innymi kryteriami (tak na szybko, bo notatki są w pracy):

  • czy da radę coś wykorzystać w innym projekcie (oszczędzić sobie pracy)
  • czy będę wymieniał komponenty na inne (uaktualniał, lub całkowicie zastępował, patrz - żywotność projektu)
  • czy ktoś inny będzie zmieniał kod (i chcę żeby mógł taplać się w swoim bagienku, aż skończy), jak bardzo chcę mu ułatwić współpracę (a sobie mękę przy merge-owaniu)
  • czy zakładam, że system przetrwa rok, dwa, pięć, dziesięć
  • jak szybko muszę wprowadzać zmiany i na ile jest to ważny system (np. mam taki jeden na utrzymaniu, gdzie wszystko jest w miarę trywialne, ale jest testowane do ^n tylko po to żebym mógł wypuszczać na produkcję w każdym momencie po zmianie kilku linijek)
  • jaki system ma potencjał na rozrośnięcie się (jak ważny jest dla biznesu)
  • czy będzie musiał udostępniać dane, współpracować z innym systemem.

W zależności od powyższych stosuje więcej lub też mniej 'dobrych' praktyk. Generalnie starając się dowozić porządnie i szybko, a żeby było szybko często muszę włożyć wysiłek w odpowiednie podzielenie projektu, testowanie, wybór komponentów na których można polegać, etc

0

Potrzebne Ci w ogóle to repozytorium? Wstrzyknij sobie dbContext do kontrolera i heja :)

1
AdamWox napisał(a):

Uważam, że więcej czasu spędza się na generowaniu tych wszystkich interfejsów do serwisów i repozytoriów niż na faktycznym programowaniu. Rozumiem jakby omówione kwestie miały wpływ na pracę aplikacji, jej szybkość, wydajność, ale to jest tylko, albo też aż, jakiś sposób na organizację projektu. Jeśli więcej osób pracuje nad jednym projektem to ok, każdy piszę tak samo i każdym wie gdzie i czego się spodziewać.

To nie jest tak, że każdy serwis potrzebuje interfejs. Jeżeli nie planujesz zmieniać bazy danych i nie przeszkadza Ci zawiłe linq w serwisach to nie potrzebujesz również dodatkowej warstwy opakowującej dbContext. Co do Twojego podejścia do testowania to się zupełnie nie zgadzam. Dla Twojego dobra i klientów twojej firmy warto się do tego przekonać. Jeżeli zdecydujesz się na testowanie tych serwisów to możesz to zrobić poprzez InMemoryDbContext lub/oraz SqliteInMemory.

PS.
Ja pracuje w bardzo małej firmie (5 osób, razem ze mną). Programowanie to tylko dodatek do usług jakie oferujemy. Ostatnio zrobiło się zapotrzebowanie na webowe aplikacje, ponieważ ludzie chcą mieć dostęp zewsząd. Przerzucając się z WinForms, to dla mnie to duża nowość. Mam już kilka projektów na swoim koncie, ale żaden z nich nie jest zrobiony w sposób w jaki tutaj opisujecie i (co dziwne) wszystko działa.

Tutaj groźniejsze jest to, że gdy będzie potrzebna nowa funkcjonalność ty lub przyszli programiści pracujący nad danym projektem mogą sobie nie poradzić w zadowalającym czasie.

0
Krzysztof Pe napisał(a):

Cześć,

Z mojej perspektywy (bardziej doświadczeni pewnie mnie poprawią):

Ad. 2. ...a po drugie w przypadku rozbudowanych projektów i np. widoku który potrzebuje danych z dwóch serwisów masz możliwość połączenia tego właśnie w repozytorium. (Tutaj może lepiej jakby ktoś bardziej doświadczony to wyjaśnił).

Tutaj mala uwaga - jest dokladnie odwrotnie. Repozytorium jest pojeciem, ktore jest zwiazane jednoznacznie i nierozerwalnie z konkretna encja i nie powinien np obslugiwac kwerend relacyjnych. W takim przypadku wykorzystujemy obiekt klasy Query. Natomiast serwis jest "rozszerzeniem" kontrolera i jego lacznikiem z repozytorium (repozytoriami w przypadku jesli kontroler tego wymaga). Logika umieszczona w serwisie umozliwia maksymalne odchudzenie kontrolera, tak by ten zajmowal sie wylacznie przekazaniem danych (np do widoku). Injekcja repozytoriow wprost do kontrolera jest bledem i uzasadnia ja wylacznie brak koniecznosci dodawania jakiejkolwiek logiki zwiazanej np z obrobka/przygotowania parametrow koniecznych do wywolania konkretnej metody repozytorium. W przypadku mapowania, transformacji itp parametrow, logika zwiazana z tymi procesami powinna zostac umieszczona w serwisie. Z poziomu kontrolera wolamy tylko o dane, wszystkie inne rzecz takie jak autentykacja, autoryzacja, walidacja itp itd od biedy mozna umiescic serwisie, chociaz to tez jest blad i nalezy wykorzystac do tego middleware.

1 użytkowników online, w tym zalogowanych: 0, gości: 1