Struktura projektu z C# i EfCore

0

Cześć,
uczę się Entity Framework Core i próbuję zrobić "bardziej zaawansowany" projekt z podziałem na kilka bibliotek. Jaką strukturę rozwiązania polecacie? Gdy używałem ADO.NET, tworzyłem zawsze:

  1. Projekt.DatabaseAccess - pobieranie connection stringów z pliku i przechowywanie bieżącego podczas sesji aplikacji
  2. Projekt.Shared - bazowe klasy dla modeli (implementujące INotifyPropertyChanged)
  3. np. Projekt.Customers - wszystkie modele dot. klientów i statyczne klasy z metodami CRUD.

Customers odwoływał się wtedy do DatabaseAccess (żeby mieć bieżącego connection stringa dla CRUDów) i do Shared.

Używając EfCore chciałem osiągnąć podobną strukturę, więc:

  1. Projekt.DatabaseAccess - pobieranie connection stringów z pliku, główny DbContext i DbContextFactory (IDesignTimeDbContextFactory) dla tworzenia migracji
  2. Projekt.Shared - bazowe klasy
  3. Projekt.Customers - modele dot. klientów i repozytorium (z podawanym DbContext w konstruktorze) dla CRUDów

I tu napotkałem się na problem - repozytorium korzysta z DbContext dla swoich operacji (czyli relacja z Projekt.DatabaseAccess), a w DbContext mam zdefiniowane DbSety, które odnoszą się do Projekt.Customers. Tworzy się błędne koło, bo w Visual Studio nie można dodać zależności Projekt A <--> Projekt B.

screenshot-20240617185900.png

Jedyne rozwiązanie jakie udało mi się wymyślić, to stworzyć kolejny projekt - Customers.Repositories, który odnosi się do DatabaseAccess i Customers. Mam jednak wrażenie, że można to zrobić jakoś lepiej.

screenshot-20240617190153.png

Zależy mi na tym, aby zachować podział na różne strefy programu (Projekt.Customers, Projekt.Planning, Projekt.HR, ....)

Czy są jakieś inne rozwiązania? Czy może istnieją lepsze sposoby podziału aplikacji?

PS: Projekt. w nazwach projektów to np. SauerkrautCRM, żeby ktoś się zaraz o to nie doczepił : )

7

Clean Architecture - Domain, Application i Infrastructure. Jest pełno przykładów w sieci z zastosowaniem MediatR i EFCore właśnie.

https://github.com/jasontaylordev/CleanArchitecture

0

Dzięki!

0

Przepraszam, że odpisuję dopiero teraz - miałem duży natłok pracy 🙂 Przeanalizowałem Twój przykład (https://github.com/jasontaylordev/CleanArchitecture), ale nie rozwiązuje on mojego głównego problemu - nie tworzy podziału na poszczególne sfery programu (np. Projekt.Customers, Projekt.Planning), a nie chcę trzymać wszystkich modeli w jednym miejscu : ).

2

Poczytaj o 'vertical slice architecture' i 'modular monolith'. Tu masz przykład https://github.com/kgrzybek/modular-monolith-with-ddd

1

@bbhzp: nic nie stoi na przeszkodzie żeby napisać moduły przy pomocy clean architecture i korzystając z extension method dopinać je przy pomocy DI

0

Dziękuję wszystkim za pomoc i za podsunięcie pomysłu z vertical slice i clean architecture. Dopiero się uczę, więc wszelkie informacje są dla mnie bardzo cenne. Nie tworzę na razie modułów, chciałem tylko podzielić swoją aplikację na kilka mniejszych projektów, aby nie trzymać wszystkiego w folderze Models : )

0

Ja nie rozumiem, dlaczego chcesz dzielić model DB na jakieś części. Jeśli chcesz mieć jakiś większy porządek to dodaj jakieś podfoldery w Models dla obszarów.
Model DB (Context, Entities...) odpowiada tylko za DB.

A dalej w aplikacji mają być osobne klasy, które mogą być podzielone na Twoje obszary, wydzielone repozytoria dla obszarów (Shared, Customers...) ale w końcu wszystko co idzie do DB przechodzi przez jeden kontekst DB z jednym, spójnym modelem DB.

Medtator i inne wynalazki nie mają znaczenia.

Jak już tam będzie 500 tabel w DB to można się zastanawiać nad wydzielaniem kontekstów ale to raczej dla dużych systemów i silnie się wiąże z mechanizmami bazy danych (schemy, widoki, triggery...).

0

Staram się tworzyć swoje projekty w taki sposób, aby były jak najbardziej skalowalne. Umiem już tworzyć aplikacje, które w jednym projekcie mają wszystko - style (WPF), kontrolki, dostęp do bazy danych, modele, itp. Programowanie to moje hobby, ale gdy przyjdzie pracować mi w zawodzie, to nie widziałem nigdy jakiejkolwiek aplikacji komercyjnej, która trzymała by wszystko w jednym miejscu - wszystkie korzystają ze swoich oddzielnych plików DLL, nawet te mniejsze : )

1
bbhzp napisał(a):

I tu napotkałem się na problem - repozytorium korzysta z DbContext dla swoich operacji (czyli relacja z Projekt.DatabaseAccess), a w DbContext mam zdefiniowane DbSety, które odnoszą się do Projekt.Customers. Tworzy się błędne koło, bo w Visual Studio nie można dodać zależności Projekt A <--> Projekt B.

Jedyne rozwiązanie jakie udało mi się wymyślić, to stworzyć kolejny projekt - Customers.Repositories, który odnosi się do DatabaseAccess i Customers. Mam jednak wrażenie, że można to zrobić jakoś lepiej.

Zależy mi na tym, aby zachować podział na różne strefy programu (Projekt.Customers, Projekt.Planning, Projekt.HR, ....)

Czy są jakieś inne rozwiązania? Czy może istnieją lepsze sposoby podziału aplikacji?

To co próbujesz tutaj zrobić, to zaprojektować architekturę aplikacji. Tylko taka próba jest skazana na porażkę, bo nie ma architektury dobrej do wszystkiego. Niektóre są lepsze do jednych programów, innych do drugich.

Więc żeby zaprojektować dobrze Twój program, to musisz to zrobić w otoczeniu use-case'ów i tego w jaki sposób użytkownicy korzystają z Twojej aplikacji. Bez tego tworzysz na ślepo, i na 99% wytworzysz design który nie pasuje do Twojego przypadku.

0

@Riddle: jakby użył clean to by wiedział gdzie ten interface ma siedzieć 😉

2
rjakubowski napisał(a):

@Riddle: jakby użył clean to by wiedział gdzie ten interface ma siedzieć 😉

Skąd miałby wiedzieć?

Takie architektury wymyślane "na zapas" rzadko przeżywają pierwsze zderzenie z featureami od użytkowników.

0
Riddle napisał(a):
bbhzp napisał(a):

I tu napotkałem się na problem - repozytorium korzysta z DbContext dla swoich operacji (czyli relacja z Projekt.DatabaseAccess), a w DbContext mam zdefiniowane DbSety, które odnoszą się do Projekt.Customers. Tworzy się błędne koło, bo w Visual Studio nie można dodać zależności Projekt A <--> Projekt B.

Jedyne rozwiązanie jakie udało mi się wymyślić, to stworzyć kolejny projekt - Customers.Repositories, który odnosi się do DatabaseAccess i Customers. Mam jednak wrażenie, że można to zrobić jakoś lepiej.

Zależy mi na tym, aby zachować podział na różne strefy programu (Projekt.Customers, Projekt.Planning, Projekt.HR, ....)

Czy są jakieś inne rozwiązania? Czy może istnieją lepsze sposoby podziału aplikacji?

To co próbujesz tutaj zrobić, to zaprojektować architekturę aplikacji. Tylko taka próba jest skazana na porażkę, bo nie ma architektury dobrej do wszystkiego. Niektóre są lepsze do jednych programów, innych do drugich.

Więc żeby zaprojektować dobrze Twój program, to musisz to zrobić w otoczeniu use-case'ów i tego w jaki sposób użytkownicy korzystają z Twojej aplikacji. Bez tego tworzysz na ślepo, i na 99% wytworzysz design który nie pasuje do Twojego przypadku.

Dzięki za odp. Tworzę mały/średni, desktopowy program CRM skierowany tak naprawdę tylko do jednej, może trzech osób. Nie planuję w nim obsługi "dogrywanych" modułów, każdy użytkownik będzie miał takie same funkcje (ukrywane jedynie za pomocą systemu uprawnień, ale to nieistotne).

Gdy używałem ADO.NET stosowałem architekturę taką jak ww. poście, i byłem z niej zadowolony. Tworzyła ona dla mnie sensowny podział danych i fajnie działała z GITem:

Projekt.Customers przechowywał wszystkie modele dot. klientów i statyczną klasę, która pobierała dane z bazy i mapowała je na obiekty. Z jej poziomu odbywało się również dodawanie, edycja i usuwanie klientów.

Projekt.Planning zawierał np. obsługę wydarzeń w kalendarzu i był powiązany z Projekt.Customers, bo wydarzenie mogło mieć przypisanego klienta.

Stosując taki podział, główny projekt (w tym przypadku WPF) nie wywoływał bezpośrednio żadnych zapytań do bazy, tylko korzystał z utworzonych poprzednio bibliotek klas.

Z czasem jednak coraz więcej czasu musiałem poświęcać na ręczne pisanie podobnych do siebie SQLów, a poza tym trudne było np. dodawanie wielu wierszy na raz (mój wcześniejszy post), więc postanowiłem nauczyć się EfCore, a w nim nie mogę osiągnąć takiej samej architektury.

0
bbhzp napisał(a):

Dzięki za odp. Tworzę mały/średni, desktopowy program CRM skierowany tak naprawdę tylko do jednej, może trzech osób. Nie planuję w nim obsługi "dogrywanych" modułów, każdy użytkownik będzie miał takie same funkcje (ukrywane jedynie za pomocą systemu uprawnień, ale to nieistotne).

Super, jakie to będą funkcje konkretnie? Te funkcje powinny być Twoim punktem wyjścia. Co użytkownicy mogą zrobić z tym systemem?

bbhzp napisał(a):

Z czasem jednak coraz więcej czasu musiałem poświęcać na ręczne pisanie podobnych do siebie SQLów, a poza tym trudne było np. dodawanie wielu wierszy na raz (mój wcześniejszy post), więc postanowiłem nauczyć się EfCore, a w nim nie mogę osiągnąć takiej samej architektury.

No to wydaje mi się że niestety nie z ADO.NET miałeś problem, tylko chyba niestety z designem aplikacji 😕 Zastąpienie ADO.NET EfCore zmieni nie wiele.

W momencie w którym w poprzednim projekcie zauważyłeś że niektóre operacje są trudne, np. to dodawanie wielu wierszy, powinieneś się zatrzymać na chwilę i zastanowić:

  • czemu dodawanie wielu wierszy jest trudne?
  • co takiego teraz jest w aplikacji, że jak chce dodać te wiele wierszy to system ze mną walczy?
  • jest gdzieś duplikacja? tight-coupling? brak separation-of-concerns?

I kiedy już zidentyfikujesz czemu dodowanie wielu wierszy jest trudne, wtedy należałoby to jakoś naprawić - albo wydzielić pod moduł, albo nałożyć abstrakcje, albo zrobić jakiś gruntowniejszy refaktor, albo w jakiś inny sposób ułatwić sobie dodawanie tych wierszy.

Zmiana technologii raczej nie pomoże zbyt wiele - nadal wytworzysz słabo zaprojektowany software, tylko że w nowszej technologii.

0

Super, jakie to będą funkcje konkretnie? Te funkcje powinny być Twoim punktem wyjścia. Co użytkownicy mogą zrobić z tym systemem?

Zarządzanie klientami, planowanie zadań, itd.

czemu dodawanie wielu wierszy jest trudne?

(...) albo nałożyć abstrakcje

Cały trud, a raczej upierdliwość : ), to tworzenie dynamicznego SQLa, na zasadzie:

StringBuilder sb = new StringBuilder("INSERT INTO ... (imie, nazwisko) VALUES ");
List<SqlParameter> parameters = new();

for (int i = 0; i < customers.Count; i++)
{
  sb.Append($"(@imie{i}, @nazwisko{i})");
  parameters.Add(new SqlParameter($"@imie{i}", customers[i].Imie));
  ...

  if (i != customers.Count - 1)
    sb.Append(", ");
  else
    sb.Append(";");
}

SqlCommand cmd = new SqlCommand(sb.ToString());
cmd.Parameters.AddRange(parameters.ToArray());
....

Stworzyłem co prawda do tego helpery, ale w Entity Framework wystarczy:

foreach (Customer c in customers)
  context.Customers.Add(c);

context.SaveChanges();

... tylko problem polega na rozdzieleniu tego na poszczególne projekty 🙂 Dowiedziałem się o Clean Architecture i szukam właśnie jakiś dobrych materiałów do nauki, ale czy naprawdę nie ma jakiegoś prostszego podziału aplikacji?

1
bbhzp napisał(a):

Staram się tworzyć swoje projekty w taki sposób, aby były jak najbardziej skalowalne. Umiem już tworzyć aplikacje, które w jednym projekcie mają wszystko - style (WPF), kontrolki, dostęp do bazy danych, modele, itp. Programowanie to moje hobby, ale gdy przyjdzie pracować mi w zawodzie, to nie widziałem nigdy jakiejkolwiek aplikacji komercyjnej, która trzymała by wszystko w jednym miejscu - wszystkie korzystają ze swoich oddzielnych plików DLL, nawet te mniejsze : )

Ale podział aplikacji, zaplanowanie infrastruktury to nie jest podział jednej warstwy (DB) na kilka dll.
Masz warstwę dostępu do bazy gdzieś w Infrastrukture. Tam są wszystkie mechanizmy związane z DB: klasy dbentity, context, migracje...
Ta warstwa odpowiada tylko za komunikację z bazą danych.
Inne warstwy, moduły itp nie zawierają klas z warstwy DB. Jeśli w DB masz tabelę Customers która jest mapowana na CustomerEntity to klasa CustomerEntity jest używana tylko w tej warstwie.
Jeśli chcesz pokazać W UI tego Customer, żeby go dodać czy edytować, to nie używasz w UI klasy CustomerEntity tylko jakichś CustomerView, CustomerEdit itp., zmapowanej z CustermerEntity.

Jeśli masz Mediatorowe GetCustomersQuery, AddCustomerCommand to te metody nie nie zwracają klas z DB (CustomerEntity) tylko mapują CustomerEntity je na klasy używane w UI czy gdzie s w logice. Wtedy nie masz tego problemu, że CustomerEntity musi być w jakimś innym DLL niż inne klasy modelu DB.

Z tego co piszesz tu

Projekt.Customers przechowywał wszystkie modele dot. klientów i statyczną klasę, która pobierała dane z bazy i mapowała je na obiekty. Z jej poziomu odbywało się również dodawanie, edycja i usuwanie klientów.

Projekt.Planning zawierał np. obsługę wydarzeń w kalendarzu i był powiązany z Projekt.Customers, bo wydarzenie mogło mieć przypisanego klienta.

wynika (chyba), że używasz w logice aplikacji klas db, to podstawowy błąd. Jak się od tego nie odkleisz to dalej nie pójdziesz. Będzie coraz gorsza łatanina.

Podsumowawując cały model DB w jednej dll. Inne obszary/moduły w innych dll (jeśli tak chcesz) ale w tych innych modułach własne klasy reprezentujące klasy z DB.

1

Czyli w sumie całość problemu się rozchodzi o to że nie chcesz ręcznie sklejać SQL'i, tylko chciałbyś jakąś abstrakcję na to.

1
bbhzp napisał(a):

Cały trud, a raczej upierdliwość : ), to tworzenie dynamicznego SQLa, na zasadzie: (...)

Do tego jest Dapper :)

0

Dziękuję wszystkim za odpowiedzi. To jakie rozwiązanie proponujecie dla mojego przypadku (mała/średnia aplikacja CRM, bez dogrywalnych, luźnych modułów - dla każdego usera taka sama)? Jestem otwarty na duże zmiany, wszystko robię dla siebie i nie mam na razie żadnego doświadczenia zawodowego 🙂 Kilka Osób pisało o Clean Architecture, @Riddle i @jacek.placek proponują, żeby inne warstwy nie wiedziały o istnieniu DB, za wszystkie sugestie dziękuję.

To jak się za to wszystko zabrać? W Projekt.Customers trzymać tylko encje dot. klientów, ale repozytoria w Projekt.DatabaseAccess? Mieć podwójne modele (jedno dla DB, drugie dla UI)? Ja mogę tylko powiedzieć, jak robiłem to do tej pory: dla każdej sfery programu oddzielny projekt obejmujący logikę, CRUDy do bazy i modele, jeden projekt do przechowywania bieżącego connection stringa i helperów do tworzenia dynamicznych zapytań, a aplikacja kliencka nie miała pojęcia o bazie danych - używała tylko metod AddCustomer, itp.

1
bbhzp napisał(a):

To jak się za to wszystko zabrać?

Jaki jest teraz Twój faktyczny problem? Czy tym problemem jest to, że w Twoich helperach AddCustomer nie chcesz sklejać SQL'i? Jeśli tak, to po prostu zamień implementację helperów na jakieś rozwiązanie gotowe do tego, tak żeby użytkownicy tych helperów nie musieli widzieć o tej zmianie.

Błędem byłoby teraz usunąć te helpery i zastąpić je EfCore'm. Możesz dodać EfCore jak chcesz, ale do środka tych helperów (nie poza nie).

PS: Gwoli ścisłości, to co Ty nazywasz "helperami do SQLi", ja nazwałbym Twoim interfejsem na persystencję. I to jest dobre że je zrobiłeś.

bbhzp napisał(a):

a aplikacja kliencka nie miała pojęcia o bazie danych - używała tylko metod AddCustomer, itp.

No i super. Bardzo dobrze.

0
snowflake2137 napisał(a):
bbhzp napisał(a):

Cały trud, a raczej upierdliwość : ), to tworzenie dynamicznego SQLa, na zasadzie: (...)

Do tego jest Dapper :)

Jak powiedział Dwight Schrute z the Office, "One crisis at a time" : ) A tak na serio, dzięki za propozycję, też o nim czytałem!

0
Riddle napisał(a):
bbhzp napisał(a):

To jak się za to wszystko zabrać?

Jaki jest teraz Twój faktyczny problem? Czy tym problemem jest to, że w Twoich helperach AddCustomer nie chcesz sklejać SQL'i? Jeśli tak, to po prostu zamień implementację helperów na jakieś rozwiązanie gotowe do tego, tak żeby użytkownicy tych helperów nie musieli widzieć o tej zmianie.

Błędem byłoby teraz usunąć te helpery i zastąpić je EfCore'm. Możesz dodać EfCore jak chcesz, ale do środka tych helperów (nie poza nie).

PS: Gwoli ścisłości, to co Ty nazywasz "helperami do SQLi", ja nazwałbym Twoim interfejsem na persystencję. I to jest dobre że je zrobiłeś.

bbhzp napisał(a):

a aplikacja kliencka nie miała pojęcia o bazie danych - używała tylko metod AddCustomer, itp.

No i super. Bardzo dobrze.

Będąc szczerym, chciałem po prostu nauczyć się EfCore, bo zauważyłem, że pojawia się on w wielu miejscach, i był mi on nawet sugerowany w moich poprzednich postach. Nie chciałem uparcie stać przy jednej technologii, bo lepiej znać dwie niż jedną : ). Prawda jest jednak taka, że użytkownika końcowego nie obchodzi zastosowana technologia, tylko funkcjonalność programu - może on być napisany nawet w Delphi albo COBOLu : ) A dla programisty liczy się wygoda, a w ADO.NET czuję się komfortowo :). Na naukę EfCore przyjdzie jeszcze czas, bo muszę uzupełnić braki w innych miejscach : )

Dziękuję wszystkim za pomoc, Wasze odpowiedzi były bardzo przydatne :)

1

@bbhzp To możesz zrobić drugi, nowy, throw-away projekt i sobie popróbować efcore, i kiedy już się trochę poduczysz, wtedy możesz zdecydować czy dodać go do głównego projektu czy nie.

1

Ja mam tak złożony projekt i w zupełności starcza

App.Mvc
App.EntityFramework
App.Core
App.Core.Shared
App.Public
App.Mobile

App.Mvc - Aplikacja internetowa, serwisy
App.EntityFramework - baza danych (DbContext)
App.Core - Modele z bazy danych oraz klasy dostępne tylko dla aplikacji intenetowej i serwisów
App.Core.Shared - Enumeratory, stałe itp.
App.Public - Interfejsy serwisów oraz modele dla serwisów
App.Mobile - Aplikacja mobilna mająca dostęp do App.Public i App.Core.Shared

2

@bbhzp Nie idź tą drogą. EF to podstawa w .net. Pomijam, ze są tez inne ORMy. Jakby mi ktoś chciał coś pisać w ado.net w 2024 to raczej bym z nim nie gadał dłużej.

A wydaje mi się, że Twój problem w ogóle nie związku z EF. EF, czy jakakolwiek inna metoda dostępu do danych, nie ma żadnego znaczenia dla architektury aplikacji.
Wrzuć wszystko co ma związek z Db (EF context, migracje, klasy itp) w jeden projekt. Dodaj referencję do tego projektu w innych projektach (Web, Services czy co tam chcesz mieć).
Ściągnij sobie projekt podany przez @rjakubowski i zobacz gdzie są db entities i ogólnie co jest w Domain, co jest w Application itp.
Pomijam DDD w tym projekcie ale tam widać gdzie są i jak są używane db entity, gdzie jest db context, jego konfiguracja i migracje, gdzie jest konfiguracja db entities, co i jak jest zwracane w mediatorowych queries, co jest używane w widokach MVC itd.

1

Od razu uwaga. Jeśli używasz ORMa typu Entity Framework, to miej na uwadze, że to już jest repozytorium. Więc dokładając drugą warstwę repozytorium robisz coś kompletnie bez sensu. Powinieneś mieć po prostu kontekst wstrzyknięty do jakiegoś serwisu i tyle. Bez dodatkowego repozytorium.
Oczywiście, jeśli używasz mikro ORMów, jak Dapper, to wtedy repozytorium ma już sens.

A co do samego podzielenia. Ja wydzielam sobie projekt DAL - Data Access Layer, w którym mam model bazodanowy z konfiguracją no i DbContext.

0
Juhas napisał(a):

Od razu uwaga. Jeśli używasz ORMa typu Entity Framework, to miej na uwadze, że to już jest repozytorium. Więc dokładając drugą warstwę repozytorium robisz coś kompletnie bez sensu. Powinieneś mieć po prostu kontekst wstrzyknięty do jakiegoś serwisu i tyle. Bez dodatkowego repozytorium.

Jeśli chce się bawić w DDD, to repozytorium mieć raczej musi - tylko to musi być repozytorium, a nie DAO.

1
somekind napisał(a):
Juhas napisał(a):

Od razu uwaga. Jeśli używasz ORMa typu Entity Framework, to miej na uwadze, że to już jest repozytorium. Więc dokładając drugą warstwę repozytorium robisz coś kompletnie bez sensu. Powinieneś mieć po prostu kontekst wstrzyknięty do jakiegoś serwisu i tyle. Bez dodatkowego repozytorium.

Jeśli chce się bawić w DDD, to repozytorium mieć raczej musi - tylko to musi być repozytorium, a nie DAO.

No i jak DDD to też dobrze by było mieć jako taki CQRS, co by nie ciągnąć agregatów z dna bazy razem z mułem, tylko po to aby wyświetlić tabelkę z przykładowo listą zamówień pogrupowanych po kliencie...

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