I have a lack of understanding of the DDD aggregate topic.
I do have an Offer
aggregate that has navigation property to its children's collection OfferProducts
.
When I learned entity framework I thought I should always define navigation properties on both sides of the relation but Ardalis (maintainer of Specification package for ef https://github.com/ardalis/Specification) wrote somewhere these words which I do not understand correctly:
You want to avoid having navigation properties that span Aggregates. So you need to decide where navigation properties should go, and where non-navigation key properties should go instead.
This is how I designed my entities:
public class Offer : BaseEntity, IAggregateRoot
{
...
public ICollection<OfferProduct> OfferProducts { get; private set; } = new List<OfferProduct>();
public Guid InquiryId { get; private set; }
public virtual Inquiry Inquiry { get; private set; } = default!;
}
public class OfferProduct : BaseEntity, IAggregateRoot
{
...
public Guid OfferId { get; private set; }
public virtual Offer Offer { get; private set; } = default!;
public Guid InquiryProductId { get; private set; }
public virtual InquiryProduct InquiryProduct { get; private set; } = default!;
}
public class Inquiry : BaseEntity, IAggregateRoot
{
...
public ICollection<Offer> Offers { get; private set; } = new List<Offer>();
public ICollection<InquiryProduct> Products { get; private set; } = new List<InquiryProduct>();
}
public class InquiryProduct : BaseEntity, IAggregateRoot
{
...
public Guid InquiryId { get; private set; }
public virtual Inquiry Inquiry { get; private set; } = default!;
public ICollection<OfferProduct> OfferProducts { get; private set; } = new List<OfferProduct>();
}
Ardalis is saying that navigation properties should be defined only on one side. I do not know if it is because of some DDD principles or maybe because it has some performance drawbacks?
Repository from your Ardalis specification package only works with aggregate root.
OfferProduct
entities are created only with the Offer
entity and are never updated.
InquiryProduct
entities are created only with the Inquiry
entity and are never updated.
I have a business use case where I need to fetch OfferProducts
not only belonging to one Offer
but filtered by InquiryProductId
so I thought the easiest way will be to mark the OfferProduct
entity with IAggregateRoot interface and query it from the repository directly. But I think it's cheating and it's not correct because if I understand correctly AggregateRoot should be the only one and I should always query from the root.
I could fetch it from the Inquiry
aggregate root but then my specification would have to be that complex:
public class InquiryProductOffersSpec : Specification<Inquiry, InquiryDetailsDto>, ISingleResultSpecification
{
public InquiryProductOffersSpec(Guid inquiryId, Guid productId) =>
Query
.Where(i => i.Id == inquiryId)
.Include(i => i.Products.Where(ip => ip.Id == productId))
.ThenInclude(ip => ip.OfferProducts);
}
This probably would be more correct from the DDD perspective but the query will be less performant than simple select * from OfferProducts where inquiryProductId = 'someId'
So my questions are:
should I remove IAggregateRoot
from InquiryProduct
and OfferProduct
entities and fetch only from the Inquiry
entity?
why it is better to keep navigation properties only on one side of the relation?
maybe my entities and relations are designed incorrectly and that's why I am struggling with that complex query?
I will introduce the operation of the system: The system can create inquiries with its InquiryProducts, then there can be offers created for each inquiry and each offer can have some OfferProducts related to the InquiryProduct.
When writing it thought came to my mind that maybe the only AggregateRoot should be the Inquiry entity as any of the other entities can't exist without Inquiry. But In the system, I also need to fetch(search) offers independently of inquiry and I couldn't do it if I won't mark Offer with an IAggregateRoot interface.