I'm new in DDD previously I was working more with services and transaction scripts. Now I'm working with a team on a DDD project and from my point of view there is too much unnecessary data is loading and easy to forget about some data to load. I hope someone can help me to understand if there something wrong in this DDD project or it's me who just doesn't get it.
Assuming that we domain model and entity Order with relation. To change order status should I load all its relations and then all nested relations? (example 1)
If I load only the relation that I need isn't then my Order not fully created object that can cause some issue later?(example 2)
Also if I do not load every relation (because there are a lot of nested relations) operation may throw exception or just wrong result, but how I can possibly know which relation are need without checking method implementation? (example 3)
public class Application
{
SomeDbContext _context;
public Application(SomeDbContext context)
{
_context = context;
}
public void RunSomeLogic()
{
///example 1
var orders = _context.Orders
.Include(x => x.Invoices).ThenInclude(x => x.Payments)
.Include(x => x.Customer)
.Include(x => x.VerifyByUser).Where(x => x.Status == OrderStatus.Paid).ToList();
foreach (var order in orders)
order.RefreshStatus();
_context.SaveChanges();
///example 2
var orders2 = _context.Orders
.Include(x => x.Invoices).ThenInclude(x => x.Payments)
.Include(x => x.VerifyByUser).Where(x => x.Status != OrderStatus.Paid).ToList();
foreach (var order in orders)
order.RefreshStatus();
_context.SaveChanges();
///example 3
var orders3 = _context.Orders
.Include(x => x.Invoices)//.ThenInclude(x => x.Payments)
.Include(x => x.Customer)
.Include(x => x.VerifyByUser).Where(x => x.Status == OrderStatus.Paid).ToList();
foreach (var order in orders)
order.RefreshStatus();
_context.SaveChanges();
}
}
public enum OrderStatus
{
New,
Veryfied,
Paid
}
public class User
{ }
public class Order
{
public ICollection<Invoice> Invoices { get; set; }
public OrderStatus Status { get; set; }
public User VerifyByUser { get; set; }
public Customer Customer { get; set; }
public DateTime OrderDater { get; set; }
public Guid Id { get; set; }
public void RefreshStatus()
{
if (Status == OrderStatus.New && VerifyByUser != null)
Status = OrderStatus.Veryfied;
else if (Status == OrderStatus.Veryfied )
{
foreach (var invoice in Invoices)
invoice.CheckPayments();
if( Invoices.All(x => x.IsPaid))
Status = OrderStatus.Paid;
}
}
}
public class Invoice
{
public Order Order { get; set; }
public int OrderId { get; set; }
public ICollection<Payment> Payments { get; set; }
public bool IsPaid { get; set; }
public decimal Value { get; set; }
public void CheckPayments()
{
if (Payments?.Sum(x => x.Value) >= Value)
IsPaid = true;
else
IsPaid = false;
}
}
public class Payment
{
public Invoice Invoice { get;set; }
public int InvoiceId { get; set; }
public decimal Value { get; set; }
///code
}
public class Customer
{
///code
}
Polling loops to do something like update a status /w a DDD approach is bound to get expensive. One option to help cut back the cost of eager loading all of that data would be to leverage a Split Query.
A DDD approach for something like this would be instead to ensure that an Invoice has a method to "MakePayment" which the the only way that the system allows a Payment entity to to be added to the invoice, refreshing the invoice's status, and then signalling to it's order if it's status has changed. Even then you will need to know what entity relationships a DDD operation (MakePayment) is going to need and either have those eager loaded or accept the cost of lazy loading them. Ultimately this is best done at the time you are making changes to a single aggregate root rather than polling through a set of aggregate roots.
If the system needs to accommodate other services etc. adding payment records to the database, then you need to consider an approach and a supporting data structure that can easily identify these new payment records, load them with their respective invoice & order they apply to, and proceed to use the same logic to refresh those statuses. This would involve looking at something like a CreatedAt DateTime and likely a indicator of some kind on the payment to mark rows that were inserted by a alternate system and need additional processing.