Domain driven design 事件源Saga实现

Domain driven design 事件源Saga实现,domain-driven-design,event-sourcing,saga,eventstoredb,Domain Driven Design,Event Sourcing,Saga,Eventstoredb,我已经写了一个事件源聚合,现在实现了一个事件源传奇。。。我注意到这两个是相似的,并且创建了一个事件源对象作为基类,这两个对象都是从基类派生的 我在这里看过一个演示,但觉得可能有一个问题,因为在进程崩溃的情况下,命令可能会丢失,因为命令的发送在写事务之外 public void Save(ISaga saga) { var events = saga.GetUncommittedEvents(); eventStore.Write(new UncommittedEventStrea

我已经写了一个事件源聚合,现在实现了一个事件源传奇。。。我注意到这两个是相似的,并且创建了一个事件源对象作为基类,这两个对象都是从基类派生的

我在这里看过一个演示,但觉得可能有一个问题,因为在进程崩溃的情况下,命令可能会丢失,因为命令的发送在写事务之外

public void Save(ISaga saga)
{
    var events = saga.GetUncommittedEvents();
    eventStore.Write(new UncommittedEventStream
    {
        Id = saga.Id,
        Type = saga.GetType(),
        Events = events,
        ExpectedVersion = saga.Version - events.Count
    });

    foreach (var message in saga.GetUndispatchedMessages())
        bus.Send(message); // can be done in different ways

    saga.ClearUncommittedEvents();
    saga.ClearUndispatchedMessages();
}
相反,我使用的是Greg Young的EventStore,当我保存EventSourcedObject(聚合或saga)时,顺序如下:

  • 存储库获取新的MutatingEvents列表
  • 将它们写入流
  • 当流写入并提交到流时,EventStore会触发新事件
  • 我们从EventStore中侦听事件并在EventHandler中处理它们
  • 我正在实施一个传奇的两个方面:

  • 接收事件,该事件可能会转换状态,而转换状态又可能会发出命令
  • 在将来的某个时候(通过外部定时器服务)我们可以被呼叫回来时发出警报)
  • 问题

  • 据我所知,事件处理程序不应发出命令(如果命令失败会发生什么?)-但我同意上面的说法吗?因为Saga是通过此事件代理控制命令创建(对事件作出反应)的实际工具,任何命令发送失败都可以从外部处理(在处理
    CommandEmittedFromSaga
    并在命令失败时重新发送的外部事件处理程序中)

  • 或者我忘记包装事件并将本机
    命令
    事件
    存储在同一个流中(与基类消息混合在一起-Saga将同时使用命令和事件,聚合只使用事件)

  • 网络上还有其他关于事件源传说的参考资料吗?有什么我能理智地检查我的想法的吗

  • 下面是一些背景代码

    Saga发出要运行的命令(包装在CommandEmittedFromSaga事件中)

    下面的命令包装在事件中:

    public class CommandEmittedFromSaga : Event
    {
        public readonly Command Command;
        public readonly Identity SagaIdentity;
        public readonly Type SagaType;
    
        public CommandEmittedFromSaga(Identity sagaIdentity, Type sagaType, Command command)
        {
            Command = command;
            SagaType = sagaType;
            SagaIdentity = sagaIdentity;
        }
    }
    
    Saga在将来的某个时候请求回调(AlarmRequestedBySaga事件)

    报警回调请求被包装在事件旁边,并将在请求的时间或之后对事件进行回击:

    public class AlarmRequestedBySaga : Event
    {
        public readonly Event Event;
        public readonly DateTime FireOn;
        public readonly Identity Identity;
        public readonly Type SagaType;
    
        public AlarmRequestedBySaga(Identity identity, Type sagaType, Event @event, DateTime fireOn)
        {
            Identity = identity;
            SagaType = sagaType;
            Event = @event;
            FireOn = fireOn;
        }
    }
    
    或者,我可以将命令和事件存储在相同的基本类型消息流中

    public abstract class EventSourcedSaga
    {
        protected EventSourcedSaga() { }
    
        protected EventSourcedSaga(Identity id, IEnumerable<Message> messages)
        {
            Identity = id;
    
            if (messages == null) throw new ArgumentNullException(nameof(messages));
    
            var count = 0;
    
            foreach (var message in messages)
            {
                var ev = message as Event;
                var command = message as Command;
    
                if(ev != null) Transition(ev);
                else if(command != null) _messages.Add(command);
                else throw new Exception($"Unsupported message type {message.GetType()}");
    
                count++;
            }
    
            if (count == 0)
                throw new ArgumentException("No messages provided");
    
            // All we need to know is the original number of events this
            // entity has had applied at time of construction.
            _unmutatedVersion = count;
            _constructing = false;
        }
    
        readonly IEventDispatchStrategy _dispatcher = new EventDispatchByReflectionStrategy("When");
        readonly List<Message> _messages = new List<Message>();
        readonly int _unmutatedVersion;
        private readonly bool _constructing = true;
        public readonly Identity Identity;
    
        public IList<Message> GetMessages()
        {
            return _messages.ToArray();
        }
    
        public void Transition(Event e)
        {
            _messages.Add(e);
            _dispatcher.Dispatch(this, e);
        }
    
        protected void SendCommand(Command c)
        {
            // Don't add a command whilst we are in the constructor. Message
            // state transition during construction must not generate new
            // commands, as those command will already be in the message list.
            if (_constructing) return;
    
            _messages.Add(c);
        }
    
        public int UnmutatedVersion() => _unmutatedVersion;
    }
    
    公共抽象类EventSourcedSaga
    {
    受保护的EventSourcedSaga(){}
    受保护的EventSourcedSaga(标识id,IEnumerable消息)
    {
    身份=身份;
    如果(messages==null)抛出新的ArgumentNullException(nameof(messages));
    var计数=0;
    foreach(消息中的var消息)
    {
    var ev=作为事件的消息;
    var命令=消息作为命令;
    如果(ev!=null)转换(ev);
    else if(command!=null)\ u消息。添加(command);
    else抛出新异常($“不支持的消息类型{message.GetType()}”);
    计数++;
    }
    如果(计数=0)
    抛出新ArgumentException(“未提供消息”);
    //我们需要知道的是今年的原始事件数量
    //实体在构建时已申请。
    _未变异版本=计数;
    _构造=假;
    }
    readonly IEventDispatchStrategy\u dispatcher=新事件DispatchByReflectionStrategy(“何时”);
    只读列表_messages=new List();
    只读int_未经修改的版本;
    私有只读bool\u=true;
    公共只读标识;
    公共IList GetMessages()
    {
    返回_messages.ToArray();
    }
    公共无效转换(事件e)
    {
    _添加(e);
    _调度员。调度(本,e);
    }
    受保护的void SendCommand(命令c)
    {
    //在构造函数中时不要添加命令。消息
    //施工期间的状态转换不得产生新的
    //命令,因为这些命令已经在消息列表中。
    如果(_)返回;
    _添加(c);
    }
    public int UnmutatedVersion()=>\u UnmutatedVersion;
    }
    
    我认为前两个问题是对流程经理的错误理解造成的(又名Sagas,请参见底部的术语注释)

    改变你的想法 你似乎正试图将它(就像我曾经做过的那样)建模为一个反向聚合。问题是:聚合的“社会契约”是它的输入(命令)可以随时间变化(因为系统必须能够随时间变化),但它的输出(事件)不能。一旦写入,事件就是历史问题,系统必须始终能够处理它们。有了这个条件,就可以从不可变的事件流可靠地加载聚合

    如果您尝试将输入和输出作为process manager实现进行反转,则其输出不能成为记录问题,因为随着时间的推移,命令可能会被弃用并从系统中删除。当您尝试使用已删除的命令加载流时,它将崩溃。因此,建模为反向聚合的process manager无法重新加载liably从一个不变的消息流中重新加载。(我相信你能想出一个方法……但这明智吗?)

    因此,让我们考虑通过查看流程管理器所取代的内容来实现流程管理器。例如,一名管理订单履行等流程的员工。您为该用户所做的第一件事是在UI中设置一个视图,供他们查看。第二件事是在UI中创建按钮,供用户执行响应wha的操作他们在view.Ex.上看到:“此行已
    付款失败
    ,因此我单击
    取消订单
    。此行已
    付款成功
    订单项出库
    ,因此我单击
    更改订单
    。此订单