Событие CQRS выбрасывается во время применения

У меня возникла проблема с моделированием и внедрением системы посещаемости мероприятий с использованием CQRS. Моя проблема в том, что дочерняя сущность может вызвать событие, но я не уверен, как и когда его обрабатывать.

По сути, у мероприятия могут быть участники, которые начинают в состоянии TBD, и могут принять или отклонить участие в мероприятии. Однако они могут изменить свою посещаемость, и когда это произойдет, я хотел бы, чтобы событие было инициировано, чтобы обработчик событий мог обработать (например, уведомить организатора мероприятия).

Я использовал шаблон состояния для управления состоянием посетителя, и от текущего состояния зависит, должно ли быть инициировано событие. На данный момент это событие не меняет состояние События. Однако мне кажется, что это событие должно быть частью потока событий.

Моя проблема в том, что я не знаю, возникнет ли событие, пока я не применю одно из событий AttendeeResponded, которое вызывает метод в текущем состоянии. Если я инициирую событие во время применения Apply, у меня возникнет проблема регидратации. AR. Я мог бы добавить эту информацию к событию во время применения, имея возвращаемую информацию о состоянии, но тогда событие стало бы изменяемым.

Я думаю, что, возможно, шаблон состояния не работает как место, где могут быть сгенерированы события, или что, возможно, шаблон состояния здесь не подходит. Я мог бы расширить состояние, чтобы иметь метод, который определяет, вызовет ли определенное изменение состояния событие, но это кажется неуклюжим.

Наконец, в моем AR нет ссылок на eventBus, поэтому я не могу просто передать событие на шину и не включить его в поток событий AR. Мне казалось, что AR, имеющая ссылку на шину событий, начинала нарушать SRP, но, возможно, я ошибаюсь в этом.

Я включил упрощенный код, чтобы помочь моему описанию. Кто-нибудь может дать полезные советы? Спасибо, Фил

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
}

Ответ на ответ mynkow:

  1. Я показываю некоторые состояния (только для чтения), чтобы я мог создавать прогнозы текущего состояния агрегата. Как бы вы обычно это делали? Вы создаете проекцию непосредственно из событий (это кажется более сложным, чем чтение текущего состояния из агрегата) или у вас есть агрегат для создания DTO?

  2. Раньше у меня был public void AddAttendee (Guid memberID), но я переключаю его на Member, чтобы попытаться заставить существовать действительный член. Я думаю, что ошибался, делая это, и с тех пор создал диспетчера посещаемости, который выполняет эту проверку и вызывает этот метод. (код обновлен, чтобы отразить это)

  3. Я использовал вложенные классы, чтобы попытаться показать, что это отношения родитель-потомок, но я согласен, мне не очень нравится, насколько большим он делает класс Event. Однако AttendenceResponseState является вложенным, так что он может изменять частное состояние посетителя. Как вы думаете, такое использование допустимо? (код обновлен, чтобы переместить участника за пределы класса события)

Чтобы быть ясным, AttendenceResponseState - это реализация шаблона состояния, а не полное состояние участника (противоречивые слова :))

И я согласен с тем, что Attendee на самом деле не обязательно должен быть объектом, но идентификатор получен из другой системы, с которой мне нужно работать, поэтому я подумал, что буду использовать его здесь. Некоторые вещи теряются при подготовке кода для SO.

Мне лично не нравится отделять агрегатное состояние от агрегированного, это просто вопрос личного вкуса. Я мог бы пересмотреть этот выбор, если мне нужно будет реализовать momento или по мере того, как я наберусь опыта :). Также порты такие же, как и саги?

Не могли бы вы подробнее рассказать о том, как агрегат может производить более одного события? Думаю, это одна из вещей, которую я пытаюсь сделать. Можно ли вызвать ApplyEvent, затем выполнить дополнительную логику и, возможно, вызвать ApplyEvent во второй раз?

Спасибо за ваш вклад, и если у вас есть другие заметки, я буду рад их выслушать.


person Phillip    schedule 22.05.2014    source источник
comment
Как вы упомянули, события не должны применяться внутри государства. Взгляните на это, пока я еще раз прочитаю ваш вопрос. github.com/ Старейшины / Cronus / tree / master / Cronus.Persistence.MSSQL /   -  person mynkow    schedule 23.05.2014
comment
Создавайте свои прогнозы только на основе данных о событиях. Используйте в событиях простые типы, такие как string, int и т. Д., И объекты значений. Чтобы избежать конфликтов, когда я передаю идентификаторы одного AR другому, я создаю отдельные классы для каждого идентификатора, например MemberId = ›github.com/Elders/Cronus/blob/master/Cronus.Persistence.MSSQL/. Порты - это не саги. У всех свое определение саги, я считаю, что сага - это нечто, порожденное событиями из двух или более ограниченных контекстов. Я стараюсь избегать отношений между родителями и детьми, когда могу.   -  person mynkow    schedule 25.05.2014
comment
Мое первое приложение cqrs / ddd использует запросы по всей модели чтения внутри проекции для обновления. Когда я попытался воспроизвести события, произошла катастрофа. Теперь в прогнозах я никогда не запрашиваю ничего, кроме модели dto, над которой работает текущая проекция.   -  person mynkow    schedule 25.05.2014
comment
приятного чтения. Я добавил еще материал, посвященный вашим вопросам.   -  person mynkow    schedule 01.08.2014


Ответы (1)


Я обращусь к вещам, которые мне не нравятся. Это не значит, что это правильный способ сделать это.

  1. У вашего агрегата есть общедоступные свойства, раскрывающие какое-то состояние. Я бы их удалил.
  2. public void AddAttendee(Member member) Член IF - это еще один агрегат, на который я бы ссылался с помощью агрегированного идентификатора, а не типа члена. public void AddAttendee(MemberId member)
  3. Для меня это выглядит слишком сложной реализацией со всеми вложенными классами.

Агрегат должен отвечать за проверку входящих данных на соответствие состоянию (состояние - это другой класс без логики, например DTO). Агрегат также создает и собирает новые производимые события. Когда операция завершена, обработчик команд сохраняет все незафиксированные события. Если операция прошла успешно, события публикуются.

Помните, что одна команда должна обновлять только один агрегат и вызывать только один метод агрегирования, но обновление агрегата может вызвать одно или несколько событий.

Сделайте все свои проекции идемпотентными.

Используйте порты (это обработчики событий, которые обрабатывают события из текущего ограниченного контекста или другого ограниченного контекста и создают команды для текущего ограниченного контекста) для обновления нескольких агрегатов или обработки событий из других ограниченных контекстов. Порты могут запрашивать только модель чтения и создавать команды, но никогда не обновлять модель чтения оттуда.

Я редко использую Entities в своей модели. Почти все, что я проектирую, достигается с помощью агрегатов и объектов-значений. Винить меня :).

Возможно, этот ответ не соответствует вашим ожиданиям. Я просто хотел поделиться некоторыми знаниями, которые мне подходят. У меня в производстве 2 системы, соблюдающие эти правила. Просто с меньшим количеством ошибок. Я буду рад, если эта информация будет иметь какую-либо ценность для вас или других, читающих это.

Удачного кодирования

РЕДАКТИРОВАТЬ: какой-то код. Пожалуйста, прочтите комментарии. Кроме того, я не вижу никакой ценности в использовании классов Attendee. Больше информации, пожалуйста.

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);
    }      
}

Ответить => Ответить на ответ mynkow:

1) Я скопирую всю необходимую мне информацию из состояния агрегата в событие и опубликую это событие. Обработчик событий, который создает DTO и сохраняет их в базе данных для обслуживания пользовательского интерфейса, называется проекцией. Вы можете поиграть со словами и называть этот DTO проекцией. Но простое правило здесь: НИКАКИХ ВНУТРЕННИХ СОЕДИНЕНИЙ, НИКАКОГО ВЫБОРА ИЗ ДРУГОЙ ТАБЛИЦЫ. Вы можете сохранять, выбирать, обновлять информацию только из одной таблицы.

2) Guid работает какое-то время. Использование типа AR действительно плохо. Создайте объект значения, который представляет AR ID.

3) Это верно, пока только корень Aggregate заботится обо всех инвариантах, включая связанные объекты.

Государственный образец => красиво. Я использую тот же => https://github.com/Elders/Cronus/tree/master/Cronus.Persistence.MSSQL/src/Elders.Cronus.Sample.IdentityAndAccess/Accounts

Entity vs ValueObject => Лучший пример. Я всегда использую его, когда учу юниоров => http://lostechies.com/joeocampo/2007/04/15/a-discussion-on-domain-driven-design-entities/

Представьте, что клиент покупает что-то на сайте электронной коммерции. Он тратит 100 долларов в месяц. У вас может быть правило, что если у вас 10 консекв. месяцев с покупками на сумму> 100 долларов вы прикрепляете подарок к заказу клиента. Таким образом, у вас может быть больше одного события. И вот где на самом деле живет интересное. ;)

person mynkow    schedule 23.05.2014
comment
Отличный ответ, спасибо за ответ, я обновил базу вопросов по вашим пунктам. (Слишком долго, чтобы комментировать.) Спасибо, Фил. - person Phillip; 23.05.2014
comment
Я просмотрел множество документов и примеров кода CQRS, но они кажутся повсюду простыми / сложными и реализуемыми. Я просмотрел ваш код на git hub еще немного, мне потребовалось немного времени, чтобы найти UserProjection. Теперь я понимаю, что вы имеете в виду, и это довольно крутая идея. Я погуглил и ничего не увидел о доверии клиенту, но определенно следует доверять обработчикам команд. У меня есть масса других комментариев к вашим комментариям и вопросам, но, поскольку это не форум, я благодарю вас за вашу помощь. Вы дали мне много пищи для размышлений. - person Phillip; 27.05.2014
comment
Добавляйте свои вопросы в новые темы и связывайте их с этим, используя комментарии. - person mynkow; 29.05.2014