I am working on a project to help learn DDD and am trying to do CQRS and Event Sourcing. Code is in C#.
For this example, lets say I have 2 aggregates, Customer
and Product
. My aggregate repository has a get method Get<TAggregate>(Guid id)
that loads all events for that ID, creates a blank TAggregate
instance, and then replays all the events against that instance. The aggregate ignores events it doesn't know what to do with.
The below will work as expected, each aggregate is reconstituted from its events in the event store
Customer customer = repo.Get<Customer>(customerId);
Product product = repo.Get<Product>(productId);
However, if I try to reconstitute an aggregate from a collection of events from a different aggregate, this currently doesn't throw any errors, but the aggregate instance ignores any events that it doesn't know what to do with so it is left in this "clean" state as if zero events were passed to it.
Customer customer = repo.Get<Customer>(productId);
Product product = repo.Get<Product>(customerId);
I see two ways to solve this problem: - The aggregate itself checks to make sure it is in a valid state prior to allowing any domain opperations. - Event types are explicitly associated with specific aggregate types, events passed to the wrong aggregate type result in an exception.
The aggregate ensures it is in a valid state
Ex:
public class Product : AggregateRoot
{
private Guid _id;
private bool _isConstructed;
public Product(Guid id, ...)
{
// enforce domain rules here
ApplyChange(new ProductAddedEvent(id, ...));
}
public void UpdatePrice(decimal newPrice)
{
if(!_isConstructed)
throw new Exception(...);
// enforce domain rules here
ApplyChange(new ProductPriceUpdatedEvent(_id, newPrice));
}
private void Apply(ProductAddedEvent e)
{
_id = e.Id;
_isConstructed = true;
}
private void Apply(ProductPriceUpdatedEvent e)
{
...
}
}
This could work, but I feel like it can get out of hand very quickly, resulting in bulky, awkward code.
Event types are explicitly associated with specific aggregate types
Similar setup to the above example.
public abstract class Event<TAggregate> where TAggregate : AggregateRoot
{
public bool IsValidFor(AggregateRoot aggregate)
{
return aggregate is TAggregate;
}
}
public class ProductAddedEvent : Event<Product>
{
...
}
public abstract class AggregateRoot
{
public void Reconstitute(Event[] events)
{
foreach(Event event in events)
{
if(!event.IsValidFor(this)
throw new Exception(...);
ApplyEvent(event);
}
}
}
This approach makes the most sense to me. Is there some potential smell here that I am not seeing? Is there something else I'm not considering?
Edit:
One other idea I've had is perhaps that command handler needs to validate the command, querying the read model to ensure that an aggregate of expected type exists with the specified ID. But even if this ends up being the correct approach, is there anything wrong with associating events to specific aggregate types?