Przepływ danych w architekturze cebulowej

0

Hej,
poniżej z grubsza struktura projektu oraz uproszczony przykład pobrania zamówienia
jeśli chodzi o db założenia są takie, że widok to dane z ERP ( inna baza na tym samym serwerze sql ), tabele wiadomo.

Core

  • entity ( tabelki, widoki, zdarza się powiązanie między nimi .... ef ogarnia widok as entity ) - generalnie anemiczne ... to raczej CRUD
  • Interfejsy repozytoriów

Infrastruktura

  • implementacje interfejsów z Core ( tutaj używam Dapper do pobrania widoków/encji )
  • tutaj trzymam też DTO, które przyjmuje lub oddaje // to chyba było by dobrze przenieść do Serwisów?
  • DAO - dbContext
  • Migracje

Serwisy

  • serwisy operuja na repozytoriach, mapują na DTO w tą i z powrotem

WebAPI

  • kontrolery odpalają serwisy

Chce trzymać się zasady, że dane do bazy wrzucam przez EF a pobieram Dapperem.
Nie wiem jak ugryźć temat na przykładzie zamawiania towarów:
Order: id, number, date, customerId, ...
OrderPositions: id, sku, name, ....
Od strony dodawania zamówienia wszystko jest ok ... kontroler dostaje request, odpala serwis, serwis mapuje na obiekty domenowe, używa repozytorium - dodawanie działa.

tutaj moje schody
teraz chciałbym pobrać listę zamówień z nazwą kontrahenta i nie wiem gdzie to umieścić:
dapperem poleci query zmapowane na obiekt np. OrdersResponse

SELECT o.number, o.date, c.name FROM Orders o JOIN Customers c on o.customerId = c.id 

pytanie jak to ładnie zapisać

a) tak naprawdę zapytanie sql zwróci mi jakiś obiekt nie będący obiektem domenowym - nie mogę tego umieścić w moich repozytoriach ( bo operują na domenie )
b) czy w warstwie Infrastruktury umieścić jakiś DAL i klasy realizujące takie operacje, zwracające dane? czy ok jest potem wstrzykiwanie do serwisów konkretnej klasy ( builder.Services.AddScoped<OrderService>(); )? nie widzę sensu tworzenia interfejsów pod to

  • dodatkowe pytanie o "encja na twarz" - w przypadku "widok to encja", nie ma sensu przemapowywania tego na jakiś dto i w kontrolerze leci encja. czy to jest ok?

nie chciałbym teraz modyfikować mojej struktury a dodać to co potrzebuję.

0
john_doe napisał(a):

Serwisy

  • serwisy operuja na repozytoriach, mapują na DTO w tą i z powrotem

To w takim razie też są repozytoriami. Repo powinno już zwrócić obiekt domenowy.

Jeśli Twoje repozytorium zwraca "surowe" dane, i potrzebujesz je przemielić przez serwis żeby zrobić z nich obiekty domenowe, to w takim razie to "repo" jest szczegółem implementacyjnym tych serwisów, i równie dobrze mogłoby być zinlien'owane, i wtedy ten service staje się repo.

john_doe napisał(a):

a) tak naprawdę zapytanie sql zwróci mi jakiś obiekt nie będący obiektem domenowym - nie mogę tego umieścić w moich repozytoriach ( bo operują na domenie )

A czemu niby nie?

Możesz.

Repozytorium właśnie od tego jest żeby ogarniać interfejs między obiektami domenowymi i persystencją.

john_doe napisał(a):

b) czy w warstwie Infrastruktury umieścić jakiś DAL i klasy realizujące takie operacje, zwracające dane? czy ok jest potem wstrzykiwanie do serwisów konkretnej klasy ( builder.Services.AddScoped<OrderService>(); )? nie widzę sensu tworzenia interfejsów pod to

YAGNI.

john_doe napisał(a):
  • dodatkowe pytanie o "encja na twarz" - w przypadku "widok to encja", nie ma sensu przemapowywania tego na jakiś dto i w kontrolerze leci encja. czy to jest ok?

Tak długo jak nie umieścisz w encji żadnych szczegółów infry, to to jest okej.

0

moje repo zwraca encje, która to potem jest mapowana do dto ale robi to serwis między innymi.

czy w warstwie Infrastruktury umieścić jakiś DAL i klasy realizujące takie operacje, zwracające dane? czy ok jest potem wstrzykiwanie do serwisów konkretnej klasy ( builder.Services.AddScoped<OrderService>(); )? nie widzę sensu tworzenia interfejsów pod to

nie rozumiem jak to ugryźć.
najprawdopodobniej mylę entity, domain object w tym to piszę. Chodziło mi o to, że jeśli repo działa na encjach a to co chce pobrać czystym sql`em tą encją nie jest to nie mogę w interfejcie repo w Core użyć mojego DTO ( obiektu domenowego).
Stąd próba wyjścia z tego pomysłem z cytatu.

0
john_doe napisał(a):

moje repo zwraca encje, która to potem jest mapowana do dto ale robi to serwis między innymi.

czy w warstwie Infrastruktury umieścić jakiś DAL i klasy realizujące takie operacje, zwracające dane? czy ok jest potem wstrzykiwanie do serwisów konkretnej klasy ( builder.Services.AddScoped<OrderService>(); )? nie widzę sensu tworzenia interfejsów pod to

nie rozumiem jak to ugryźć.
najprawdopodobniej mylę entity, domain object w tym to piszę. Chodziło mi o to, że jeśli repo działa na encjach a to co chce pobrać czystym sql`em tą encją nie jest to nie mogę w interfejcie repo w Core użyć mojego DTO ( obiektu domenowego).
Stąd próba wyjścia z tego pomysłem z cytatu.

Ustalmy fakty.

Co masz na myśli mówiąc "encja"?

0

dokładnie... encja to dla mnie w Core aneczmiczna klasa reprezentująca relację w bazie i widok. Chciałem odseparować Core i Infra aby działała na encjach. Serwis robi coś tam i m.in odpala repozytorium a wynik mapuje na dto i to leci do klienta

1
john_doe napisał(a):

dokładnie... encja to dla mnie w Core aneczmiczna klasa reprezentująca relację w bazie i widok. Chciałem odseparować Core i Infra aby działała na encjach. Serwis robi coś tam i m.in odpala repozytorium a wynik mapuje na dto i to leci do klienta

Jeśli napisałeś aplikację w taki sposób że Twoje encje w core reprezentują relacje w bazie i widok; to bardzo mi przykro ale to nie ma specjalnego związku z cebulą. To po prostu aplikacja bez warstw. W cebuli zależności płyną od zewnątrz do środka - nie od środka na zewnątrz. Więc jeśli coś ma być w core, to nie może wiedzieć nic o tym co jest na zewnątrz; więc z definicji to co Ty masz u siebie nie jest cebulą - nie wiem czy w ogóle ma warstwy skoro te elementy wiedzą o sobie.

encja to dla mnie w Core aneczmiczna klasa reprezentująca relację w bazie i widok - To nie jest architektura cebuli, dlatego że klasy w core mają nie wiedzieć absolutnie nic ani o bazie ani o widoku.

0

mój Core nie wie co jest na zewn. ale wie o bazie bo ją odzwierciedla.
muszę jeszcze raz to poczytać.
jakiegokolwiek githuba nie obadam to jest to tak zrobione. albo UoW jeszcze którego nie chce bo to w moim przypadku przerost
Mój dodatek do tego to erp i dapper.

0
john_doe napisał(a):

mój Core nie wie co jest na zewn. ale wie o bazie bo ją odzwierciedla.
muszę jeszcze raz to poczytać.
jakiegokolwiek githuba nie obadam to jest to tak zrobione. albo UoW jeszcze którego nie chce bo to w moim przypadku przerost
Mój dodatek do tego to erp i dapper.

No to w takim razie to nie jest architektura cebulowa. Nie wiem czy w ogóle jest to jakaś architektura.

2

Ale w jaki konkretnie sposób twój Core wie o bazie danych?
Jak masz np. jakieś adnotacje związane z konkretną biblioteką której używasz w warstwie infrastruktury no to zależności masz pomieszane i nie będzie to w 100% zgodne z architekturą onion.

Ale jak mówisz, że Core wie o bazie, ale tak na prawdę masz na myśli, że to ty wiesz o bazie, bo w Core masz encje reprezentujące widok/dane z bazy, a z technicznego punktu widzenia nie ma tam zależności do bibliotek wykorzystywanych w infrze to nie jest tak źle i bym powiedział, że to taki całkiem dobry punkt startowy. Bo jak dla mnie zaczynanie od początku z pełną separacją modeli to też przerost formy nad treścią, szczególnie w prostszych przypadkach.

0

Jak masz np. jakieś adnotacje związane z konkretną biblioteką [...] no to zależności masz pomieszane i nie będzie to w 100% zgodne z architekturą onion.

nie będzie to w 100% zgodne z architekturą onion, czyli w sumie nie będzie to zgodne z architekturą onion.

Ale jak mówisz, że Core wie o bazie, ale tak na prawdę masz na myśli, że to ty wiesz o bazie, bo w Core masz encje reprezentujące widok/dane z bazy, a z technicznego punktu widzenia nie ma tam zależności do bibliotek wykorzystywanych w infrze to nie jest tak źle i bym powiedział, że to taki całkiem dobry punkt startowy.

Nie same techniczne odniesienia są zależnościami, ale też nazewnictwo pól, format danych, decyzje o tym w jakiej formie je przechowywać, etc.

To jest bardziej delikatny temat.

2

anemiczne encje to troce antypatern (a prznajmniej zawsze mi się tak wydawało) :p. Bo na cholerę ci ta encja jak i tak na niej nic nie robisz a jedynie mapujesz tam i z powrotem.

0
Schadoow napisał(a):

anemiczne encje to troce antypatern (a prznajmniej zawsze mi się tak wydawało) :p. Bo na cholerę ci ta encja jak i tak na niej nic nie robisz a jedynie mapujesz tam i z powrotem.

To ze nie ma metod to nie znaczy, ze jedyne funkcje jakie na niej operuja to mappery :)

BTW. Czy Ty sugerujesz, ze np. wszystkie structy w C sa bezuzyteczne? ;) Bo to jest wlasnie taki odpowiednik anemicznej encji

0
some_ONE napisał(a):

Bo jak dla mnie zaczynanie od początku z pełną separacją modeli to też przerost formy nad treścią, szczególnie w prostszych przypadkach.

Dokładnie. Raz dołączyłem do projektu gdzie gość się naczytał książek o clean architecture i to co tam było to jakaś masakra:
Repository, ProjectedRepository, CrudRepository<T> cuda niewidy..
. A jak się zapytałem po co to jest taki overenginering, to odpowiadał: bo to clean architecture xD.

1
szydlak napisał(a):
some_ONE napisał(a):

Bo jak dla mnie zaczynanie od początku z pełną separacją modeli to też przerost formy nad treścią, szczególnie w prostszych przypadkach.

Dokładnie. Raz dołączyłem do projektu gdzie gość się naczytał książek o clean architecture i to co tam było to jakaś masakra:
Repository, ProjectedRepository, CrudRepository<T> cuda niewidy..
. A jak się zapytałem po co to jest taki overenginering, to odpowiadał: bo to clean architecture xD.

To nie jest argument żeby nie zaczynać z separacją modeli.

Zauważyłeś że ktoś chciał spróbować zrobić separację, ale nie umiał tego zrobić. To nie jest argument za nie stosowaniem separacji na początku; tylko za tym że powinna robić to osoba która wie co robi.

0

@Riddle:
Możliwe, dla mnie liczy się jedno pytanie. Po co to robić? Czy bez tego będzie gorzej?

0

@szydlak: w sumie sprowadzając do takiego podejścia. To czemu ludzi robią DTO ? Encja na twarz i pchasz a jak potrzebujesz ukryc dane to robisz projekcje tylko z częścia propertisów.

Tak jeszcze dodam, że sporo programistów nie wie gdzie się kończy scope orm'a i uzywaja DTO jako obrone przed przypadkowa zmianą danych fuck yea xD.

1
szydlak napisał(a):

@Riddle:
Możliwe, dla mnie liczy się jedno pytanie. Po co to robić? Czy bez tego będzie gorzej?

No tak, bez tego będzie gorzej.

Lepiej mieć elementy aplikacji z mniejszą ilością zależności niż z większą.

(Ale warto też pamiętać, o tym co mówiłeś, czyli ktoś może mieć intencje zmniejszyć zależności ale tylko pogarsza sprawę - ale to jest kwestia braku umiejętności, a nie samego podejścia).

4

Nie wiem czy rozumiem co próbujesz osiągnąć ale z tego co zrozumiałem to twoim największym problemem jest rozdzielenie zapisów od odczytów na poziomie DAL. Jeśli tak to ja bym zastąpił serwisy Mediatorem (https://github.com/jbogard/MediatR) dzięki czemu w warstwie aplikacji masz pojedyńcze handlery do command, queries oraz events.

Ponadto jeżeli nie masz złożonego modelu domenowego i jeżeli nie masz złożonej logiki domenowej to zrezygnował też z rezpozytoriów (http://commitandrun.pl/2016/05/11/Repozytorium_najbardziej_niepotrzebny_wzorzec_projektowy)

Implementacja z grubsza wyglądałaby tak.


// GDZIEŚ W WARSTWIE APLIKACJI

// ZAPIS

public class CreateOrderHandler : IRequestHandler<CreateOrder, OrderDto>
{
  private readonly MyDbContext _dbContext; // implementacja DAL za pomocą EF Core
  
  public CreateOrderHandler(MyDbContext dbContext)
  {
    _dbContext = dbContext;
  }

  public async Task<OrderDto> Handle(CreateOrder request, CancellationToken cancellationToken = default)
  {
    var order = Order.New(request.A, request.B, request.C, itd);

    _dbContext.Add(order);
    _dbContext.SaveChangesAsync(cancellationToken);
  }
}

// ODCZYT

public class FetchUserOrdersHandler : IRequestHandler<FetchUserOrders, IEnumerable<OrderListDto>
{
  private readonly IDbConnextion _dbConnection; // Implementacja DAL za pomocą Dappera
  
  public FetchUserOrders(IDbConnextion dbConnection)
  {
    _dbConnection = dbConnection;
  }

  public async Task<IEnumerable<OrderListDto>> Handle(FetchUserOrders request, CancellationToken cancellationToken = default)
  {
    var query = @"SELECT * FROM Orders";

    return _dbConnection.Query<IEnumerable<OrderListDto>>(query);
  }
}

Jak ci przeszkadzają szczegóły implementacyjne DAL w warstwie aplikacji to w Application możesz sobie zrobić interfejsy/definicje a implementacje umieścić w Infrastructure. Po prostu owrapowac sobie takie rzeczy jak connection czy db context.

1
john_doe napisał(a):

a) tak naprawdę zapytanie sql zwróci mi jakiś obiekt nie będący obiektem domenowym - nie mogę tego umieścić w moich repozytoriach ( bo operują na domenie )

Prawda.

b) czy w warstwie Infrastruktury umieścić jakiś DAL i klasy realizujące takie operacje, zwracające dane?

Tak.

czy ok jest potem wstrzykiwanie do serwisów konkretnej klasy ( builder.Services.AddScoped<OrderService>(); )? nie widzę sensu tworzenia interfejsów pod to

Tak.
Interfejsy z jedną implementacją to zawsze kodowy smród.

  • dodatkowe pytanie o "encja na twarz" - w przypadku "widok to encja", nie ma sensu przemapowywania tego na jakiś dto i w kontrolerze leci encja. czy to jest ok?

To nigdy nie jest dobry pomysł.
Co będzie, gdy w tabeli pojawi się pole, którego nie będziesz chciał wyświetlać użytkownikowi?

Tylko przy CRUDzie, w którym nie mam logiki w domenie nie bawiłbym się w żadną cebulę, tylko zrobiłbym po prostu tak:

  • kontroler woła serwis
  • serwis ma zależność od EF/Dappera i jej używa
  • profit

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