Search code examples
eventsdomain-driven-designcqrsevent-sourcingstate-pattern

CQRS event being thrown during apply


I am having a problem modeling and implementing a event attendance system using CQRS. My issue is that a child entity can raise an event, but I am not sure how and when to process it.

Basically, an event can have attendees, which start in a TBD state, and can either accept or reject attending the event. However, they can change their attendance, and when that happens, I would like a event to be raised so that an event handler can process (notify a event organizer for example).

I have used the state pattern to manage an attendee's state, and it depend on the current state whether a event should be raised. At the moment, this event does not change the state of the Event. However it seems to me that this event should be part of the event stream.

My issue is that I don't know if the event will be raised until I apply one of the AttendeeResponded events, which calls the method on the current state.If I raise an event during a Apply, then I would end up with problem rehydrating the AR. I could add this information to the event during the apply, having the state return information, but then the event become mutable.

My thought is that maybe the state pattern does not work well as a place where events could be generated, or that maybe that the state pattern is not a good fit here. I could extend the state to have a method that determines if a certain state change will throw an event, but that seems clunky.

Finally, my AR's don't have any references to eventBus's, so I can't just throw an event onto the bus, and not have it as part of the AR's event stream. I had though AR's having a reference to the event bus was starting to violate SRP, but maybe I'm wrong on that.

I've included simplified code to help my description. Anyone with some helpful tips? Thanks,Phil

public class Event : EventSourcedAggregateRoot<Guid>
{
    #region Fields
    private readonly HashSet<Attendee> _attendance = new HashSet<Attendee>();
    private Guid _eventID;
    private string _title;
    #endregion
    #region Constructors
    [Obsolete]
    private Event()
    {
    }
    public Event(LocalDate date, string title)
    {
        HandleEvent(new EventCreated(date, title, new GuidCombGenerator().GenerateNewId()));
    }
    public Event(IEnumerable<IAggregateEvent<Guid>> @events)
    {
        LoadsFromHistory(@events);
    }
    #endregion
    #region Properties and Indexers
    public IReadOnlyCollection<Attendee> Attendance
    {
        get { return _attendance.ToArray(); }
    }
    public Guid EventID
    {
        get { return _eventID; }
        private set
        {
            if (_eventID == new Guid()) _eventID = value;
            else throw new FieldAccessException("Cannot change the ID of an entity.");
        }
    }
    public LocalDate Date { get; private set; }            
    public override Guid ID
    {
        get { return EventID; }
        set { EventID = value; }
    }       
    public string Title
    {
        get { return _title; }
        private set
        {
            Guard.That(() => value).IsNotNullOrWhiteSpace();
            _title = value;
        }
    }       
    #endregion
    #region Methods
    public override void Delete()
    {
        if (!Deleted)
            HandleEvent(new EventDeleted(EventID));
    }
    public void UpdateEvent(LocalDate date, string title)
    {
        HandleEvent(new EventUpdated(date, title, EventID));
    }
    public void AddAttendee(Guid memberID)
    {            
        Guard.That(() => _attendance).IsTrue(set => set.All(attendee => attendee.MemberID != memberID), "Attendee already exists");
        HandleEvent(new AttendeeAdded(memberID, EventID));
    }
    public void DeleteAttendee(Guid memberID)
    {
        Guard.That(() => memberID).IsTrue(x => Attendance.Any(a => attendee.MemberID == memberID), "MemberID does not exist");
        HandleEvent(new AttendeeDeleted(memberID, EventID));
    }               
    internal void RespondIsComing(Guid memberID)
    {
        Guard.That(() => memberID).IsTrue(x => Attendance.Any(a => attendee.MemberID == memberID), "MemberID does not exist");
        HandleEvent(new AttendeeRespondedAsComing(memberID, EventID));
    }
    internal void RespondNotComing(Guid memberID)
    {
        Guard.That(() => memberID).IsTrue(x => Attendance.Any(a => attendee.MemberID == memberID), "MemberID does not exist");
        HandleEvent(new AttendeeRespondedAsNotComing(memberID, EventID));
    }       
    #endregion
    #region Event Handlers
    private void Apply(EventCreated @event)
    {
        Date = @event.Date;
        Title = @event.Title;
        EventID = @event.EventID;          
    }
    private void Apply(EventDeleted @event)
    {
        Deleted = true;
    }
    private void Apply(AttendeeAdded @event)
    {
        _attendance.Add(new Attendee(@event.MemberID, @event.EventID));
    }
    private void Apply(EventUpdated @event)
    {
        Title = @event.Title;
        Date = @event.Date;           
    }
    private void Apply(AttendeeRespondedAsComing @event)
    {
        var attendee = GetAttendee(@event.AttendeeID);
        attendee.Accept();
    }
    private void Apply(AttendeeRespondedAsNotComing @event)
    {
        var attendee = GetAttendee(@event.AttendeeID);
        attendee.Reject();
    }              
    private void Apply(AttendeeDeleted @event)
    {
        _attendance.RemoveWhere(x => x.AttendeeID == @event.AttendeeID);
    }
    protected override void ApplyEvent(IAggregateEvent @event)
    {
        Apply((dynamic) @event);
    }
    #endregion        
}

public class Attendee 
{
    #region AttendenceResponse enum
    public enum AttendenceResponse
    {
        TBD,
        Coming,
        NotComing
    }
    #endregion
    #region Fields
    private IAttendenceResponseState _attendState;   
    private readonly Guid _eventID;         
    private readonly Guid _memberID;
    #endregion
    #region Constructors
    public Attendee(Guid memberID, Guid EventID)
    {                
        _memberID = memberID;
        _eventID = EventID;
        _attendState = new TBD(this);
    }
    #endregion
    #region Properties and Indexers           
    public IAttendenceResponseState AttendingState
    {
        get { return _attendState; }
        private set { _attendState = value; }
    }
    public Guid EventID
    {
        get { return _eventID; }
    }           
    public Guid MemberID
    {
        get { return _memberID; }
    }           
    #endregion
    #region Methods
    public void Accept()
    {
        _attendState.Accept();
    }
    public void Reject()
    {
        _attendState.Reject();
    }                    
    #endregion
    #region Nested type: IAttendenceResponseState
    public interface IAttendenceResponseState
    {
        #region Properties and Indexers
        AttendenceResponse AttendenceResponse { get; }
        #endregion
        #region Methods
        void Accept();
        void Reject();
        #endregion
    }
    #endregion
    #region Nested type: Coming
    private class Coming : IAttendenceResponseState
    {
        #region Fields
        private readonly Attendee _attendee;
        #endregion
        #region Constructors
        public Coming(Attendee attendee)
        {
            _attendee = attendee;
        }
        #endregion
        #region IAttendenceResponseState Members
        public void Accept()
        {
        }
        public AttendenceResponse AttendenceResponse
        {
            get { return AttendenceResponse.Coming; }
        }
        public void Reject()
        {
            _attendee.AttendingState = (new NotComing(_attendee));
            //Here is where I would like to 'raise' an event
        }               
        #endregion
    }
    #endregion
    #region Nested type: NotComing
    private class NotComing : IAttendenceResponseState
    {
        #region Fields
        private readonly Attendee _attendee;
        #endregion
        #region Constructors
        public NotComing(Attendee attendee)
        {
            _attendee = attendee;
        }
        #endregion
        #region IAttendenceResponseState Members
        public void Accept()
        {
            _attendee.AttendingState = (new Coming(_attendee));
            //Here is where I would like to 'raise' an event
        }
        public AttendenceResponse AttendenceResponse
        {
            get { return AttendenceResponse.NotComing; }
        }
        public void Reject()
        {
        }                
        #endregion
    }
    #endregion
    #region Nested type: TBD
    private class TBD : IAttendenceResponseState
    {
        #region Fields
        private readonly Attendee _attendee;
        #endregion
        #region Constructors
        public TBD(Attendee attendee)
        {
            _attendee = attendee;
        }
        #endregion
        #region IAttendenceResponseState Members
        public void Accept()
        {
            _attendee.AttendingState = (new Coming(_attendee));
        }
        public AttendenceResponse AttendenceResponse
        {
            get { return AttendenceResponse.TBD; }
        }
        public void Reject()
        {
            _attendee.AttendingState = (new NotComing(_attendee));
        }
        #endregion
    }
    #endregion
}

Reply to mynkow's response:

  1. I expose some of the state (read-only mind you) so that I might create projections of the current state of an aggregate. How would you normally do this? Do you create projection directly from events (this seems more complicated then reading the current state from the aggregate), or do you have your aggregate create DTOs?

  2. I had public void AddAttendee(Guid memberID) before, but I switch it to Member to try force that a valid member would have to exist. I think I was wrong in doing this, and have since created an Attendance manager that does this validation and calls this method. (code updated to reflect this)

  3. I used nested classes to try to signify that it was parent child relationship, but I agree, I don't much like how large it makes the Event class. The AttendenceResponseState is nested however so that it can modify the Attendee's private state. Do you think this use is valid? (code updated to move Attendee outside of Event class)

Just to be clear, AttendenceResponseState is a implementation of the State Pattern, not the Attendee's full state (conflicting words :))

And I agree that Attendee doesn't really need to be an entity, but the ID is from another system that I have to work with, so I thought I would use it here. Some stuff is lost in the preparation of the code for SO.

I personally don't like separating the aggregates state from the aggregate, but just as a matter of personal taste. I might review that choice if I have to implement momento's, or as I gain more experience :). Also are Ports the same as Sagas?

Can you talk more to how an aggregate would produce more then one event? I think this is one of the things I am trying to do. Is it ok to call an ApplyEvent, then perform more logic and possibly call ApplyEvent a second time?

Thanks for your input, and if you have any other notes I'll be glad to hear them.


Solution

  • I will address the things I do not like. This does not mean it is the right way of doing it.

    1. Your aggregate has public properties exposing some kind of a state. I would remove them.
    2. public void AddAttendee(Member member) IF member is another aggregate I would reference it with the aggregate ID instead of the Member type. public void AddAttendee(MemberId member)
    3. It looks like too complicated implementation for me with all the nested classes.

    The Aggregate should be responsible for validating the incoming data against the state (the state is another class with no logic, like DTO). The aggregate also creates and collects new events which are produced. When the operation is done the command handler persists all uncommitted events. If the operation is successful the events are published.

    Remember, one command must update only one aggregate and invoke only one aggregate method, but updating the aggregate may produce 1 or more events.

    Make all your projections idempotent.

    Use Ports (these are event handlers which handle events from the current bounded context or other bounded context and produce commands for the current bounded context) to update multiple aggregates or handle events from other bounded contexts. Ports can only query the read model and produce commands, never update the read model from there.

    I rarely use Entities in my model. Almost everything I design is accomplished by aggregates and value objects. Blame me :).

    May be this answer does not meet your expectations. I just wanted to share some knowledge which works for me. I have 2 systems in production following these rules. Simple with less bugs. I will be glad if this info has any values to you or others reading this.

    Happy coding

    EDIT: Some code. Please read the comments. Also, I do not see any value using the Attendee classes. More info pls.

    public class Event : EventSourcedAggregateRoot<Guid>
    {
        private readonly HashSet<AttendeeId> _attendance = new HashSet<Attendee>();
        private EventId _eventID;
        private string _title;
    
        // generating AR ID should not be a responsibility of the AR
        // All my IDs are generated by the client or the place where commands are created
        // One thing about building CQRS systems is the you must trust the client. This is important. Google it.
        public Event(EventId id, LocalDate date, string title, List<AttendeeId> attendees/* Can you create an event without attendees? */)
        {
            HandleEvent(new EventCreated(date, title, attendees, id));
        }
    
        This override reminds me of an active record pattern.
        //public override void Delete()
        public void Cancel()
        {
            if (!Deleted)
                HandleEvent(new EventDeleted(EventID));
        }
    
        // May be you could split it to two events. The other one could be RescheduleEvent
        // and all attendees will be notified. But changing the title could be just a typo.
        public void UpdateEvent(LocalDate date, string title)
        {
            HandleEvent(new EventUpdated(date, title, EventID));
        }
    
        public void AddAttendee(AttendeeId memberID)
        {            
            Guard.That(() => _attendance).IsTrue(set => set.All(attendee => attendee.MemberID != memberID), "Attendee already exists");
            HandleEvent(new AttendeeAdded(memberID, EventID));
        }
        public void DeleteAttendee(AttendeeId memberID)
        {
            Guard.That(() => memberID).IsTrue(x => Attendance.Any(a => attendee.MemberID == memberID), "MemberID does not exist");
            HandleEvent(new AttendeeDeleted(memberID, EventID));
        }               
        internal void RespondIsComing(AttendeeId memberID)
        {
            Guard.That(() => memberID).IsTrue(x => Attendance.Any(a => attendee.MemberID == memberID), "MemberID does not exist");
            HandleEvent(new AttendeeRespondedAsComing(memberID, EventID));
        }
        internal void RespondNotComing(AttendeeId memberID)
        {
            Guard.That(() => memberID).IsTrue(x => Attendance.Any(a => attendee.MemberID == memberID), "MemberID does not exist");
            HandleEvent(new AttendeeRespondedAsNotComing(memberID, EventID));
        }       
    
        private void Apply(EventCreated @event)
        {
            Date = @event.Date;
            Title = @event.Title;
            EventID = @event.EventID;          
        }
        private void Apply(EventDeleted @event)
        {
            Deleted = true;
        }
        private void Apply(AttendeeAdded @event)
        {
            _attendance.Add(new Attendee(@event.MemberID, @event.EventID));
        }
        private void Apply(EventUpdated @event)
        {
            Title = @event.Title;
            Date = @event.Date;           
        }
        private void Apply(AttendeeRespondedAsComing @event)
        {
            var attendee = GetAttendee(@event.AttendeeID); // What this method does?
            //attendee.Accept();
        }
        private void Apply(AttendeeRespondedAsNotComing @event)
        {
            var attendee = GetAttendee(@event.AttendeeID);// What this method does?
            //attendee.Reject();
        }              
        private void Apply(AttendeeDeleted @event)
        {
            _attendance.RemoveWhere(x => x.AttendeeID == @event.AttendeeID);
        }
        protected override void ApplyEvent(IAggregateEvent @event)
        {
            Apply((dynamic) @event);
        }      
    }
    

    Reply => Reply to mynkow's response:

    1) I would copy all the information I need from the aggregate's state to the event and publish that event. The event handler which creates DTOs and stores them in database to serve UI is called projection. You may play with the words and call that DTO a projection. But the simple rule here is: NO INNER JOINS, NO SELECT FROM ANOTHER TABLE. You can save, select, update information only from one table.

    2) Guid works for some time. Using the AR type is really bad. Create a value object which represents the AR ID.

    3) It is valid as long as only the Aggregate root takes care about all the invariants including related entities.

    State pattern => nice. I use the same => https://github.com/Elders/Cronus/tree/master/Cronus.Persistence.MSSQL/src/Elders.Cronus.Sample.IdentityAndAccess/Accounts

    Entity vs ValueObject => Best example ever. I always use it when I teach the juniors => http://lostechies.com/joeocampo/2007/04/15/a-discussion-on-domain-driven-design-entities/

    Imagine that a customer buys something from eCommerce website. He spends $100 each month. You can have a rule that if you have 10 conseq. months with purchases > $100 you attach a gift to the clients order. This is how you can have more than 1 event. And this is where the interesting stuff actually lives. ;)