12

我使用域事件模式已经有一段时间了——它使我们能够在域层中封装尽可能多的行为,并为我们应用程序的其他部分订阅域事件提供了一种很好的方式。

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

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

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

正如您所看到的,这只不过IEventDispatcher是实际执行调度发布事件的工作的垫片。

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

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

这在大多数情况下都可以正常工作。但是,这种方法存在一些问题:

仅当实体成功持久化时才应分派事件

参加以下课程:

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

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

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

session.SaveChanges();

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

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

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

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

由于我不确定如何实际预先生成 RavenDB 标识符,因此一种解决方案可能是将事件的创建延迟到实际保存实体之前,但我又不是如何最好地实现这一点 - 也许排队集合Func<TEntity, TEvent>

4

2 回答 2

9

一种解决方案(如@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
    }
}

从本质上讲,这实现了与@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);
}

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

var orderId = session.GenerateIdFor<Order>();
var order = new Order(orderId, 1.99M);
于 2013-12-27T17:10:08.717 回答
4

按照我们在 Twitter 上的讨论,我不会从 Order 构造函数中这样做,而是这样做:

var order = new Order(amount: 10);
session.Store(order);
DomainEvents.Raise(new OrderRaisedEvent { OrderId = order.Id });

session.SaveChanges();

更好的是 - 您可以创建一个IDocumentStoreListener并根据持久化的类型从那里执行它。我认为没有理由在构造函数中执行此操作 - 您希望在订单被持久化(或发送以持久化)时引发事件,而不是在创建它的内存表示时。

由于这将在内部使用 HiLo 生成器,因此可以保证每次调用 Store() 时都不会访问数据库,并且 ID 将是唯一的。因此,如果出现任何问题,将收到带有订单 ID 的消息,并且在多次重试后找不到具有该 ID 的订单时,您可以假设持久化它出现问题。或者,您可以尝试捕获异常并引发另一个事件,其中包含该订单 ID 的异常详细信息。

试图在同一个事务中同时包含消息调度和存储文档在 RavenDB 中是一种矫枉过正的做法。相反,您应该计划和构建失败。

于 2013-12-27T13:39:30.510 回答