Gdzie pobierać ceny produktów na potrzeby agregatu Order w architekturze DDD / CQRS?

2

Mam dwie bazy - jedną do zapisu eventów, drugą dokumentową do odczytu.
W momencie gdy użytkownik tworzy zamówienie to muszę wypełnić danymi mój agregat Order zawierający również pozycje zamówienia z cenami:

public class Order : AggregateRoot
{
    public Guid Id { get; private set; }
    public Guid CustomerId { get; private set; }
    public List<OrderItem> OrderItems { get; private set; }

    public Order(Guid id, Guid customerId)
    {
        OrderItems = new List<OrderItem>();
    }

    public void AddOrderItem(Guid productId, int quantity, decimal price)
    {
      //...
    }
}

To wypełnianie agregatu danymi jest realizowane oczywiście w command handlerze:

public class CreateOrderCommand : IRequest
{
    public Guid CustomerId { get; }
    public List<CreateOrderItemDTO> OrderItems { get; }

    public CreateOrderCommand(Guid customerId, List<CreateOrderItemDTO> orderItems)
    {
        CustomerId = customerId;
        OrderItems = orderItems;
    }
}

public class CreateOrderItemDTO 
{
    public Guid ProductId { get; set; }

    public int Quantity { get; set; }
}

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand>
{
    private readonly IEventStoreRepository _eventStoreRepository;

    public CreateCustomerCommandHandler(
        IEventStoreRepository eventStoreRepository)
    {
        _eventStoreRepository = eventStoreRepository;
    }

    public async Task<Unit> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        var order = new Order(Guid.NewGuid(), request.CustomerId);
        foreach (var item in request.OrderItems)
        {
            order.AddOrderItem(item.ProductId, item.Quantity, /* tu potrzebuje ceny */);
        }

        await _eventStoreRepository.Save(order);

        return Unit.Value;
    }
}

Jednak potrzebuję ceny produktu. Gdzie ją powinienem pobrać? W command handlerze? Ale jeśli tak to z bazy eventów czy z bazy do odczytu? A może w kontrolerze przed wysłaniem komendy i dodać do komendy nowe pole "Price"?

Tu jeszcze mój kontroler:

[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderDTO orderDto)
{
    var orderCmd = new CreateOrderCommand(orderDto.CustomerId, orderDto.OrderItems);
    await _mediator.Send(orderCmd);
}

Na pewno nie chcę bazować na cenie, która byłaby ewentualnie w tym obiekcie CreateOrderDTO zawierającym listę produktów zamawianych przez użytkownika, bo ktoś mógłby zmanipulować tą cenę.

1

A w Order Item ta cena już nie jest dawno określona?

Mi tam nic do heksagonal arch. ale coś o operacjach biznesowych to słyszałem przez ramię ...
A już bez ironii, to olbrzymi temat.
Cena może zależeć od ilości / nie zależeć, zależeć od Customera/ daty / reszty pozycji / historii handlowej / nie zależeć. Sorry, jestem moze zbyt mocno oswojony ze swoim spojrzeniem, ale ten casus kojarzy mi się z nadmierną granulacją.

1

Uczę się właśnie DDD i buduję prostą aplikację. To składanie zamówienia, czyli to nowe zamówienie czyli w OrderItem nie ma jeszcze ceny. W przypadku edycji by była.

2

Kontroler powinien być jak 'najgłupszy' i nie powinien zawierać logiki biznesowej, a tak jak piszę @ZrobieDobrze ustalenie konkretnej ceny może nie być oczywiste i jest logiką biznesową. W przypadku gdy warstwa rest api jest usuwana i zastępowana czymś innym stracimy część logiki. Więc zdecydowanie umieściłbym w handlerze.

0

Hm......... znalazłem niemal identyczny temat na stackoverflow:
https://stackoverflow.com/questions/59991128/cqrs-how-to-handle-if-a-command-requires-data-from-db-query

oraz na jakimś forum Axon framework:
https://discuss.axoniq.io/t/what-is-the-proper-way-of-handling-command-that-need-state-from-query-model/3803

tam niby zalecają, żeby uzupełniać tę cenę w kontrolerze. Aczkolwiek to co Wy piszecie też ma sens.

0

Łączenie danych z dwóch baz, to albo handler, albo domena, jeśli to możliwe.

0

Kontoler jak i handler należą do tej samej warstwy. Jakbyś nie miał handlerów/serwisów aplikacyjnych to wtedy ten kod by wylądował w kontrolerach, a skoro masz to logikę biznesową orkiestrujesz w handlarach. Odnośnie ilości źródeł danych, im mniej tym lepiej, zawsze powinniśmy dążyć do jak największej autonomiczności. A jak już chcielibyśmy się dobrać do źródła danych posiadanego przez inny serwis, to tylko przez jego oficjalne API, a nie bezpośrednio za plecami serwisu dla którego to źródło jest przeznaczone. Jak padnie nam źródło danych do odczytów, to raczej nie chcielibyśmy żeby wyłożyło nam to przetwarzanie transakcji biznesowych.

0

U mnie w projektach, dane do logiki biznesowej zawsze pobieramy z eventów i tworzymy nowy event, który zapisujemy do bazy. Dopiero jak event trafi do bazy, to w tle działą sobie deamon, który tworzy view(dane do odczytu). Wszystko mamy w jednej bazie, jest jedna tabela z eventami, zaś view mają osobne tabele. Korzystam z MartenDB do obsługi DDD. Tam jest baza z tabelami json

0

No dobra czyli pobranie cen produktow najlepiej zrealizowac w command handlerze. Czy ja dobrze rozumiem @neves i @Michalk001 zeby nie pobierac tych cen jednak z bazy do odczytu a pobrac w tym command handlerze dane produktow z bazy z eventami dla drugiego aggregate roota o nazwie Product? Czyli cos takiego?

public async Task<Unit> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
	var order = new Order(Guid.NewGuid(), request.CustomerId);
	foreach (var item in request.OrderItems)
	{
	    var product = _eventStoreRepository.GetById<Product>(item.ProductId); // <--- cos takiego - pobranie produktu z EventStoreDB?
		order.AddOrderItem(item.ProductId, item.Quantity, product.Price);
	}

	await _eventStoreRepository.Save(order);

	return Unit.Value;
}
2
neves napisał(a):

Kontoler jak i handler należą do tej samej warstwy.

Absolutnie nie.

Jakbyś nie miał handlerów/serwisów aplikacyjnych to wtedy ten kod by wylądował w kontrolerach

Czyli w warstwie interakcji ze światem zewnętrznym. Bardzo brzydkie podejście.

0

@somekind: mogę normalnie w command handlerze pobrać ceny produktów z bazy typu read? Czy zalecane jest by tak w moim poscie wyzej pobrac je takze z bazy do zapisu - z evantami?

0

Ja nie wiem co to jest baza typu read. Dla mnie każda baza jest taka sama, i dostaję się do danych w niej przez Gateway, a tego wywołuję z Use Case. (Command handlery są u mnie implementacja Use Casów.) Jeśli są dwie bazy, to gatewaya każdej z nich użyję w handlerze.

screenshot-20221122001636.png

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