Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Events in nested entities #10

Open
darbio opened this issue Jul 31, 2020 · 5 comments
Open

Events in nested entities #10

darbio opened this issue Jul 31, 2020 · 5 comments
Assignees
Labels
enhancement New feature or request

Comments

@darbio
Copy link

darbio commented Jul 31, 2020

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).

class BankAccount {
    public IEnumerable<BankCard> Cards { get; }
    public Person Owner { get; }
    
    public void ChangeOwnerName() {
        this.Owner.SetName(...);
    }

    public void CancelBankCard(id) {
        ... get card by id
        card.Cancel();
    }
}

class BankCard {
    ...
    public void Cancel() {
        ... raise event 
    }
}

class Person {
    ...
    public void ChangeName() {
        ... raise event
    }
}

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?

@huysentruitw huysentruitw self-assigned this Jul 31, 2020
@huysentruitw huysentruitw added the question Further information is requested label Jul 31, 2020
@huysentruitw
Copy link
Owner

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?

@darbio
Copy link
Author

darbio commented Jul 31, 2020

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:

  1. Event apply methods should only set properties without ever throwing an exception
  2. We shouldn’t be checking invariants when rehydrating an Aggregate Root from events.

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.

@darbio
Copy link
Author

darbio commented Aug 1, 2020

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:

  1. Subclass all events that go onto a person (this isn't a major blocker)
  2. Register the event on the aggregate and implement a handler, as well as
  3. Register the event on the entity

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):

  1. In the aggregate root you register the entity with some kind of locator (RegisterEntity<TEntity, TEvent>(Action<TEntity, TEvent> entityLocator) ==> RegisterEntity<Person, PersonEvent>((@event, person) => person.Id == @event.personID));
  2. When an event is called, the aggregate looks for a local event hander, and if not checks the registered entities for an event handler.

@darbio
Copy link
Author

darbio commented Aug 10, 2020

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:

  • Root - finds the appropriate entity to apply the event to (locator)
  • Entity - applies the event (Handler)

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.

@huysentruitw
Copy link
Owner

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.

@huysentruitw huysentruitw added enhancement New feature or request and removed question Further information is requested labels Aug 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants