Open darbio opened 4 years ago
Thanks for your question.
Preivously, in a more complex project, I was using AggregateSource from Yves Reynhout and that library supports routing and makes a distinction between AggregateRoot's and child entities. In that complex project, we first tried to handle all events in the aggregate root, but that became pretty much unmaintainable, so we switched to using the routing capabilities of that library.
The Aggregator library is greatly inspired by my experience with Yves library, the implementation however has many differences.
Currently, I'm using this library (Aggregator) in a very simple domain where all events can remain on the aggregate root. I try to avoid opinionated implementations, and try to only implement stuff the community has agreed upon or ways that seem logical. That said, I'm always open to suggestions and ways to improve this library. I think it would be fairly easy to add the concept of a child entity and add a way to forward events from the aggregate root to the child.
Do you have experience in this matter? What were your findings?
I have a similar experience to you. I've put all the events on the aggregate root, but it's quickly getting unwieldy. I don't currently use your library, however I accidentally have a very similar implementation, so I'm thinking of ripping mine out and replacing with this one instead.
When researching my issue I found this article by Nick Chamberlain which discusses the problem.
The rules they outlined are:
Which I tend to agree with.
And in conclusion, Nick describes it as:
We introduced a pattern where we route events to the child Entity, using a constructor that doesn’t check for invariants but instead calling a command handler method on the Entity which returns the event after checking invariants. This keeps the invariant checking out of the Entity constructor used for rehydration and allows for the definition of valid conditions on the Entity rather than on the Aggregate Root.
Which appears to be the AggregateSource 'way'.
The method proposed by Mark Nijhof in his book CQRS is that the aggregate root calls LoadFromHistory(IEnumerable<DomainEvent> events)
on the Entity. I followed this in my own implementation and, whilst it works, I didn't really like it because it was a bit of a hacky implementation (IMO) - having to create a handler on the aggregate root for the event and then another on the entity for the same event before calling LoadFromHistory multiple times.
What were your thoughts on the AggregateSource implementation? I haven't tried that yet.
This is my current implementation:
public abstract class Base
{
public int Version { get; protected set; } = -1;
private readonly Dictionary<Type, Action<IDomainEvent>> registeredEventHandlers = new Dictionary<Type, Action<IDomainEvent>>();
protected Base()
{
this.RegisterEvents();
}
protected abstract void RegisterEvents();
public void Register<T>(Action<T> handler) where T : class, IDomainEvent
{
if (this.registeredEventHandlers.ContainsKey(typeof(T)))
{
throw new InvalidOperationException($"{typeof(T).Name} is already registered.");
}
this.registeredEventHandlers.Add(typeof(T), @event => handler(@event as T));
}
protected void Apply<T>(Type type, T @event) where T : class, IDomainEvent
{
if (!this.registeredEventHandlers.ContainsKey(type))
{
throw new InvalidOperationException($"{type.Name} is not registered in ${this.GetType().Name}");
}
var handler = this.registeredEventHandlers[type];
handler(@event);
}
public void LoadFromHistory(IEnumerable<IDomainEvent> events)
{
var orderedEvents = events.OrderBy(@event => @event.Version);
foreach (var @event in orderedEvents)
{
this.Apply(@event.GetType(), @event);
}
this.Version = orderedEvents.Last().Version;
}
}
public abstract class AggregateRoot : Base, IAggregateRoot
{
public Guid Id { get; protected set; } = Guid.Empty;
public bool IsCreated() => this.Version > -1;
protected Queue<IDomainEvent> events { get; } = new Queue<IDomainEvent>();
protected AggregateRoot() : base()
{
}
public void Raise<T>(T @event) where T : class, IDomainEvent
{
this.Apply(@event.GetType(), @event);
this.events.Enqueue(@event);
}
public Queue<IDomainEvent> GetEvents()
{
return this.events;
}
public void Clear()
{
this.events.Clear();
}
public int GetNextVersion()
{
return this.Version + 1;
}
}
public abstract class Entity : Base, IEntity
{
public Guid Id { get; protected set; } = Guid.Empty;
public Guid AggregateId => this.AggregateRoot.Id;
public bool IsCreated() => this.Id != Guid.Empty;
protected readonly IAggregateRoot AggregateRoot;
protected Entity(IAggregateRoot aggregateRoot) {
this.AggregateRoot = aggregateRoot;
}
protected void Raise<T>(T @event) where T : class, IDomainEvent
{
this.AggregateRoot.Raise(@event);
}
public int GetNextVersion()
{
return this.AggregateRoot.GetNextVersion();
}
}
And a contrived example:
public class Family : AggregateRoot
{
public string Name { get; private set; }
public IEnumerable<Person> Members { get; private set; } = new List<Person>();
protected override void RegisterEvents()
{
base.Register<FamilyStartedEvent>(this.onFamilyStartedEvent);
base.Register<PersonAddedToFamilyEvent>(this.onAnyEventForAPerson);
base.Register<PersonMarriedEvent>(this.onAnyEventForAPerson);
}
public void Start(string familyName)
{
Guard.Argument(familyName).NotNull().NotWhiteSpace().NotEmpty();
this.Raise(new FamilyStartedEvent(Guid.NewGuid(), this.GetNextVersion(), familyName));
}
public void AddMember(string firstName, string lastName)
{
Guard.Argument(firstName).NotNull().NotWhiteSpace().NotEmpty();
Guard.Argument(lastName).NotNull().NotWhiteSpace().NotEmpty();
var personId = Guid.NewGuid();
this.Raise(new PersonAddedToFamilyEvent(personId, this.Id, this.GetNextVersion(), firstName, lastName));
}
private void onAnyEventForAPerson(PersonEvent e)
{
var person = this.Members.SingleOrDefault(a => a.Id == e.PersonId);
if (person == null) person = new Person(this);
person.LoadFromHistory(new DomainEvent[] { e });
}
private void onFamilyStartedEvent(FamilyStartedEvent e)
{
this.Id = Guid.NewGuid();
this.Name = e.FamilyName;
}
}
public class Person : Entity
{
public enum MaritalStatus
{
Single,
DeFacto,
Married,
Divorced
}
public string FirstName { get; set; }
public string LastName { get; set; }
public MaritalStatus Status { get; set; } = MaritalStatus.Single;
public Person(Family aggregateRoot) : base(aggregateRoot)
{
}
protected override void RegisterEvents()
{
base.Register<PersonAddedToFamilyEvent>(this.onPersonAddedToFamilyEventPersonInitializedEvent);
base.Register<PersonMarriedEvent>(this.onPersonMarriedEvent);
}
public void AddToFamily(string firstName, string lastName)
{
Guard.Argument(firstName).NotNull().NotEmpty().NotWhiteSpace();
Guard.Argument(lastName).NotNull().NotEmpty().NotWhiteSpace();
this.Raise(new PersonAddedToFamilyEvent(Guid.NewGuid(), this.AggregateId, this.GetNextVersion(), firstName, lastName));
}
public void Marry()
{
this.Raise(new PersonMarriedEvent(this.Id, this.AggregateId, this.GetNextVersion(), MaritalStatus.Married.ToString()));
}
private void onPersonAddedToFamilyEventPersonInitializedEvent(PersonAddedToFamilyEvent e)
{
this.FirstName = e.PersonFirstName;
this.LastName = e.PersonLastName;
}
private void onPersonMarriedEvent(PersonMarriedEvent e)
{
this.Status = Enum.Parse<MaritalStatus>(e.MaritalStatus);
}
}
family = new Family();
family.Start("Jones");
family.AddMember("Joe", "Jones");
var joe = family.Members.Single(a => a.FirstName == "Joe");
joe.Marry();
I don't like the onAnyEventForAPerson
pattern because I have to:
Additionally, calling 'LoadFromHistory', whilst it works, it does seem smelly!
It would be nicer to have an event router that does this 'magically', such as (I'm adlibbing here):
RegisterEntity<TEntity, TEvent>(Action<TEntity, TEvent> entityLocator)
==> RegisterEntity<Person, PersonEvent>((@event, person) => person.Id == @event.personID)
);I had a play and came to the conclusion that registering the event on both the root and the entity actually isn't an anti-pattern, if you do it right...
Mark Nijhof's 'onAnyEventForA...' and using 'LoadHistory' is slightly smelly - the 'LoadFromHistory' method gets misused (in my opinion) by the root.
Nick Chamberlain proposes that the Entity returns the event from a static method, which we apply to both the root and the entity. The root locates the entity and then calls the 'Apply' method on the entity.
This seems like double handling, and it is, but for a good reason - the handlers do different things:
So, what this means:
Creation: I have a static method on the entity class that returns an event. The event is raised on the root, and subsequently gets routed to the entity for application.
Mutation: The root calls a method on the entity, which raises an event that gets routed back to the root. The event is then routed back to the entity for application.
This is Nick Chamberlains algorithm.
Can you think of a more elegant solution? It is rather complex to wrap your head around.. but it just works TM.
Thank you very much for this research while I'm on a holiday 🙂
I'm convinced that aggregates or entities should not have to care about loading their history themselves, the state should be pushed to the entities through the aggregate root.
What you describe is in essence how I used Yves library in that bigger, more complex project. In the aggregate root constructor, we had Register() calls, but also ForwardTo(). The latter uses the router functionality to route events to the nested entity, we had a ForwardTo overload that also worked on collections where you could pass an expression that explains how the nested entity was selected from the collection given the incoming event.
The nested entity could also call Apply which appends the event to the stream of the aggregate root.
So, I'm glad you came to the same conclusion and this is actually something we can add to Aggregator. I will be picking this up when I'm back home.
Using this library, how do you suggest handling events in child entities?
For example, in the following (made up) banking domain, a bank account (aggregate root) is registered in a person’s name (entity) and can have multiple bank cards (entities).
When a person changes their name, the entity raises an event to say that they have changed their name.
When a bank card is cancelled, the bank card entity raises an event to say it has been cancelled.
Should these bubble up to the root? Or should the root know how to apply events to an entity? If so, how does the root know such entity to apply this to?
I’ve seen other libraries suggest that all events remain on the aggregate root (e.g. BankAccountCardCancelledEvent), others on both (CardCancelledEvent on the card, BankAccountCardCancelledEvent on root), and others using routing from the root to the entity.
Any tips?