延迟域事件的创建和发送

Ben*_*ter 12 c# messaging domain-driven-design ravendb domain-events

我一直在使用域事件模式 - 它使我们能够在域层中尽可能多地封装行为,并为我们的应用程序的其他部分订阅域事件提供了一种很好的方法.

目前我们正在使用一个静态类,我们的域对象可以调用它来引发事件:

static class DomainEvents
{
    public static IEventDispatcher Dispatcher { get; set; }

    public static void Raise<TEvent>(TEvent e)
    {
        if (e != null)
        {
            Dispatcher.Dispatch(e);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,这仅仅是一个IEventDispatcher实际执行调度发布事件的垫片.

我们的调度程序实现只是使用我们的Io​​C容器(StructureMap)来定位指定类型的事件的事件处理程序.

public void Dispatch<TEvent>(TEvent e)
{
    foreach (var handler in container.GetAllInstances<IHandler<TEvent>>())
    {
        handler.Handle(e);
    }
}
Run Code Online (Sandbox Code Playgroud)

这在大多数情况下都可以.但是,这种方法存在一些问题:

仅在成功持久保存实体时才应分派事件

参加以下课程:

public class Order
{
    public string Id { get; private set; }
    public decimal Amount { get; private set; }

    public Order(decimal amount)
    {
        Amount = amount;
        DomainEvents.Raise(new OrderRaisedEvent { OrderId = Id });
    }
}
Run Code Online (Sandbox Code Playgroud)

Order构造函数中,我们提出了一个OrderRaisedEvent.在我们的应用程序层,我们可能会创建订单实例,将其添加到我们的数据库"session",然后提交/保存更改:

var order = new Order(amount: 10);
session.Store(order);

session.SaveChanges();
Run Code Online (Sandbox Code Playgroud)

这里的问题是在我们成功保存Order实体(提交事务)之前引发域事件.如果保存失败,我们仍然会调度事件.

更好的方法是将事件排队,直到实体被持久化.但是,我不确定如何在保持强类型事件处理程序的同时实现这一点.

在实体持久化之前,不应创建事件

我面临的另一个问题是在存储实体(RavenDB - session.Store)之前不会设置/分配我们的实体标识符.这意味着在上面的示例中,实际传递给事件的订单标识符null.

由于我不确定如何能够预先实际生成RavenDB标识符,因此一种解决方案可能是延迟事件的创建,直到实际实际保存为止,但同样我不是最好实现这一点 - 可能排队一组Func<TEntity, TEvent>

Ben*_*ter 8

一个解决方案(由@synhershko建议)是将域事件的调度移到域外.这样我们就可以确保在提出任何事件之前我们的实体是持久的.

但是,我们现在将行为从域(它所属的域)移到我们的应用程序中,只是为了解决我们的持久性技术 - 我并不高兴.

如果实体成功持久化,则只应调度我的事件解决方案,即创建一个对事件进行排队的延迟事件调度程序.然后我们将调度程序注入我们的工作单元,确保我们首先持久保存我们的实体,然后发出域事件:

public class DeferredEventDispatcher : IEventDispatcher
{
    private readonly IEventDispatcher inner;
    private readonly ConcurrentQueue<Action> events = new ConcurrentQueue<Action>();

    public DeferredEventDispatcher(IEventDispatcher inner)
    {
        this.inner = inner;
    }

    public void Dispatch<TEvent>(TEvent e)
    {
        events.Enqueue(() => inner.Dispatch(e));
    }

    public void Resolve()
    {
        Action dispatch;
        while (events.TryDequeue(out dispatch))
        {
            dispatch();
        }
    }
}

public class UnitOfWork
{
    public void Commit()
    {
        session.SaveChanges();
        dispatcher.Resolve(); // raise events
    }
}
Run Code Online (Sandbox Code Playgroud)

基本上这实现了与@synhershko建议相同的事情,但保持在我的域内"提升"事件.

至于实体被持久化之前不应该创建事件主要问题是实体标识符是由RavenDB在外部设置的.使我的域持久无知且易于测试的解决方案是简单地将id作为构造函数参数传递.如果使用SQL数据库(通常传递Guid),这就是我要做的.

幸运的是,RavenDB确实为您提供了使用hilo策略生成标识符的方法(因此我们可以保留RESTful标识符).这来自RavenDB Contrib项目:

public static string GenerateIdFor<T>(this IAdvancedDocumentSessionOperations session)
{
    // An entity instance is required to generate a key, but we only have a type.
    // We might not have a public constructor, so we must use reflection.
    var entity = Activator.CreateInstance(typeof(T), true);

    // Generate an ID using the commands and conventions from the current session
    var conventions = session.DocumentStore.Conventions;
    var databaseName = session.GetDatabaseName();
    var databaseCommands = session.GetDatabaseCommands();
    return conventions.GenerateDocumentKey(databaseName, databaseCommands, entity);
}
Run Code Online (Sandbox Code Playgroud)

然后,我可以使用它来生成ID并将其传递给我的实体构造函数:

var orderId = session.GenerateIdFor<Order>();
var order = new Order(orderId, 1.99M);
Run Code Online (Sandbox Code Playgroud)