我在使用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的回复:
我公开了一些状态(请注意,您是只读的),以便我可以创建聚合当前状态的投影。您通常会如何做?您是直接从事件中创建投影(这似乎比从聚合中读取当前状态要复杂得多),或者您有聚合来创建DTO?
我之前具有公共无效的AddAttendee(Guid memberID),但我将其切换为Member以尝试强制必须存在有效成员。我认为这样做是错误的,此后创建了一个出勤管理器来执行此验证并调用此方法。 (更新代码以反映这一点)
我使用嵌套类来尝试表明这是父子关系,但是我同意,我不太喜欢Event类的大小。但是,嵌套了AttendenceResponseState,以便可以修改Attendee的私有状态。您认为这种用法有效吗? (已更新代码以将与会者移到Event类之外)
请注意,AttendenceResponseState是状态模式的实现,而不是参与者的完整状态(冲突的单词:))
[我同意,参加者并不一定要是一个实体,但是ID是我必须使用的另一个系统的,所以我认为我会在这里使用它。在编写SO代码时会丢失一些东西。
我个人不喜欢将聚合状态与聚合状态分开,而只是出于个人喜好。如果必须实施momento的方法,或者随着我获得更多的经验,我可能会回顾一下该选择。端口也和Sagas一样吗?
您能否谈一谈聚合将产生多于一个事件的方式?我认为这是我要尝试做的事情之一。可以先调用ApplyEvent,然后执行更多逻辑并可能再次调用ApplyEvent吗?
感谢您的输入,如果您还有其他注释,我将很高兴听到他们的发言。
我将解决我不喜欢的事情。这并不意味着它是正确的方法。
public void AddAttendee(Member member)
IF成员是另一个聚合,我将使用聚合ID而不是Member type来引用它。 public void AddAttendee(MemberId member)
聚合应该负责根据状态验证传入的数据(状态是另一个没有逻辑的类,例如DTO)。聚合还创建并收集产生的新事件。操作完成后,命令处理程序将保留所有未提交的事件。如果操作成功,则事件将被发布。
记住,一个命令只能更新一个聚合并且只能调用一个聚合方法,但是更新聚合可能会产生1个或多个事件。
使所有投影成为幂等。
使用端口(这些是事件处理程序,用于处理来自当前有界上下文或其他有界上下文的事件并为当前有界上下文生成命令)以更新多个聚合或处理来自其他有界上下文的事件。端口只能查询读取模型并生成命令,而不能从那里更新读取模型。
我很少在模型中使用实体。我设计的几乎所有内容都是通过集合和值对象来完成的。怪我:)。
可能是这个答案不符合您的期望。我只是想分享一些对我有用的知识。我遵循这些规则在生产中有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并将其存储在数据库中以服务UI的事件处理程序称为投影。您可以使用这些单词,然后将该DTO称为投影。但是这里的简单规则是:没有内部联接,没有从另一个表中选择。您只能从一个表中保存,选择,更新信息。
2)Guid工作了一段时间。使用AR类型确实很糟糕。创建一个代表AR ID的值对象。
3)有效,只要只有聚合根负责所有不变量,包括相关实体。
状态模式=>很好。我使用相同的=> 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个conseq。如果购物金额超过$ 100个月,您就会在客户订单上附加一份礼物。这样您可以举办多个活动。这就是有趣的东西实际存在的地方。 ;)