dotnet-architecture / eShopOnWeb

Sample ASP.NET Core 8.0 reference application, powered by Microsoft, demonstrating a layered application architecture with monolithic deployment model. Download the eBook PDF from docs folder.
https://docs.microsoft.com/dotnet/standard/modern-web-apps-azure-architecture/
MIT License
10.2k stars 5.55k forks source link

How shared entities in aggregate? #747

Closed marcrib closed 1 year ago

marcrib commented 2 years ago

Hi

How shared entities in aggregate folder?

In DDD entity is referenced with key, but how share and use entity framework lazy loading composition?

ardalis commented 2 years ago

I'm not sure what lazy loading has to do with anything here. You shouldn't be using lazy loading in web applications, in any case: https://ardalis.com/avoid-lazy-loading-entities-in-asp-net-applications/

marcrib commented 2 years ago

Hi @ardalis. Nice article, but I must have expressed myself badly

My question is how to share the entity between aggregates and can use EF Core compositing to get data.

For example, how I search for order list containing the buyer name?

BuyerAggregate OrderAggregate

in order this method: GetOrders()

database.Orders.Select(o => new OrderDTO {
     Id = o.Id,
     BuyerName = x.Buyer.Name // How use composition for get this data?
    ... Other properties}).ToList();

Buyer entity is not valueobject

If don't use EF compositing, is it right to do something like this?

  public async Task<List<OrderDTO>> GetOrdersAsync()
    {
     //this code is based in orderService, in  CatalogItemOrdered

    var orders = await _orderRepository.GetAll();

        var buyerSpecification = new GetBuyerNameSpecification(orders.Select(o => o.BuyerId).ToArray());
        var buyers = await _itemRepository.ListAsync(buyerSpecification );

        return orders.Select(order => new OrderDTO
        {
            BuyerName = buyers.First(b =>b.Id == order.BuyerId).Name;

        }).ToList();
    }
ks1990cn commented 2 years ago

Hey @marcrib ,

Yes, it's totally fine.. in fact you are doing same thing...

In First case, its directly querying over Orders list, where as you are first getting all the values first and then you are querying.

You can make .Join() and get result in one go!

LiangZugeng commented 1 year ago

Hi @marcrib,

I'm new to this reference app, and I'm also in the progress of learning DDD too.

eShopOnWeb app is a reference app, it doesn't have a use case that requires orders containing buyer's name.

In your example, Order and Buyer are two separate aggregate roots, they are managed separately, if your use case needs to display a list of orders with the buyer's name, your options are:

  1. Introduce a copy of several key info from buyer aggregate root into order aggregate root, you can add buyer's name or other information in Order entity, this way when you query the orders the buyer's names are there. This sounds like an anti-pattern when you consider it from database normalization perspective but it's a valid option in DDD design, depending on your business requirements, you may also want to maintain a copy of buyer's name in orders in case the buyer changed the name and you want to maintain a snapshot of data when the order was made.

    public class Order
    {
    // common order properties
    public int BuyerId { get; }
    public string BuyerName { get; } 
    }
  2. In src/Web project, in the handler which processes the order list query command, before returning order view models, use the code you already provided to query buyer entities and attach buyer name to order view models. If your use case is to display a list of orders in a table with one column being buyer name, the results is a paged list, so it should be OK to query limited number of buyer data (say 10, 20 or 50) every time you retrieve the orders data.

  3. In your UI, after getting a list orders, query buyer endpoint of the web api to get buyer's data using buyer Ids and organize the order data view there, again, if your use case is to display a list of orders in pages, the performance is also acceptable especially when you choose to get a list of buyers with a Id array in a single web api call.

  4. You can still create a navigation property between two aggregate roots as you see fit.

    public class Order
    {
    public int BuyerId { get; }
    public Buyer Buyer { get; }
    }

    This way when you query orders you can also include buyer data, but this is an anti-pattern since in DDD two aggregate roots should only be referenced by Ids. When to break pattern is totally up to you and depends on your use case.

As you can see, the option you may choose depends solely on your use case, each of them have different performance impacts, code complexities and maintainability.

ardalis commented 1 year ago

One more point: although it's not demonstrated currently in the sample, it's OK to have query objects focused on read-only or ad hoc search scenarios. You don't need an aggregate for every query that you might want to make - aggregates and their associated models are designed to encapsulate business complexity, which generally manifests around mutations to the model. Queries are read-only and are a frequently a different requirement entirely. Create a customer query object or service as needed and have it return a DTO with just the results you're interested in rather than trying to perform gymnastics on your aggregates to get the data back.