35

我的 ASP.NET MVC3 / NHibernate 应用程序需要触发和处理与我的域对象相关的各种事件。例如,一个Order对象可能有类似OrderStatusChanged或的事件NoteCreatedForOrder。在大多数情况下,这些事件会导致发送电子邮件,因此我不能只将它们留在 MVC 应用程序中。

我已经阅读了 Udi Dahan 的Domain Events和许多其他关于如何做这类事情的想法,我决定使用基于 NServiceBus 的主机来处理事件消息。我已经做了一些概念验证测试,这似乎运作良好。

我的问题是哪个应用程序层实际上应该引发事件。在成功持久化相关对象之前,我不想触发事件(如果持久化失败,则无法发送创建便笺的电子邮件)。

另一个问题是,在某些情况下,事件与聚合根下的对象相关联。在上面的示例中,Note通过将 a 添加到Order.Notes集合并保存订单来保存它。这带来了一个问题,因为很难评估Order保存 an 时应该触发哪些事件。我想避免在保存更新的副本之前提取对象的当前副本并查找差异。

  • UI 是否适合引发这些事件?它知道发生了哪些事件,并且只有在服务层成功保存对象后才能触发它们。让控制器触发域事件似乎有些不对劲。

  • 成功持久化后存储库是否应该触发事件?

  • 我应该完全分离事件,让存储库存储一个Event对象,然后由轮询服务获取,然后变成 NServiceBus 的事件(或直接从轮询服务处理)?

  • 有一个更好的方法吗?也许只有在对象被持久化后才让我的域对象排队由服务层触发的事件?

  • 更新:我有一个服务层,但是让它通过比较过程来确定在保存给定的聚合根时应该触发哪些事件似乎很麻烦和过度。因为其中一些事件是细粒度的(例如“订单状态已更改”),我想我必须检索对象的数据库副本,比较属性以创建事件,保存新对象,然后将事件发送到 NServiceBus保存操作成功完成。

更新

在我在下面发布的答案之后(在下面),我最终做的是在我的域实体中构建EventQueue一个List<IDomainEvent>. 然后,我添加了对域进行更改的事件,这使我能够将逻辑保留在域内,我认为这是合适的,因为我是根据实体内发生的事情触发事件。

然后,当我将对象持久保存在我的服务层中时,我会处理该队列并将事件实际发送到服务总线。最初,我计划使用使用身份 PK 的遗留数据库,因此我必须对这些事件进行后处理以填充实体的 ID,但我最终决定切换到Guid.Comb允许我跳过该步骤的 PK。

4

6 回答 6

10

我的解决方案是在域层和服务层都引发事件。

您的域名:

public class Order
{
    public void ChangeStatus(OrderStatus status)
    {
        // change status
        this.Status = status;
        DomainEvent.Raise(new OrderStatusChanged { OrderId = Id, Status = status });
    }

    public void AddNote(string note)
    {
        // add note
        this.Notes.Add(note)
        DomainEvent.Raise(new NoteCreatedForOrder { OrderId = Id, Note = note });
    }
}

您的服务:

public class OrderService
{
    public void SubmitOrder(int orderId, OrderStatus status, string note)
    {
        OrderStatusChanged orderStatusChanged = null;
        NoteCreatedForOrder noteCreatedForOrder = null;

        DomainEvent.Register<OrderStatusChanged>(x => orderStatusChanged = x);
        DomainEvent.Register<NoteCreatedForOrder>(x => noteCreatedForOrder = x);

        using (var uow = UnitOfWork.Start())
        {
            var order = orderRepository.Load(orderId);
            order.ChangeStatus(status);
            order.AddNote(note);
            uow.Commit(); // commit to persist order
        }

        if (orderStatusChanged != null)
        {
            // something like this
            serviceBus.Publish(orderStatusChanged);
        }

        if (noteCreatedForOrder!= null)
        {
            // something like this
            serviceBus.Publish(noteCreatedForOrder);
        }
    }
}
于 2011-12-12T04:50:53.453 回答
7

领域事件应该在……领域引发。这就是为什么它们是领域事件。

public void ExecuteCommand(MakeCustomerGoldCommand command)
{
    if (ValidateCommand(command) == ValidationResult.OK)
    {
        Customer item = CustomerRepository.GetById(command.CustomerId);
        item.Status = CustomerStatus.Gold;
        CustomerRepository.Update(item);
    }
}

(然后在 Customer 类中,如下):

public CustomerStatus Status
{
    ....
    set
    {
        if (_status != value)
        {
            _status = value;
            switch(_status)
            {
                case CustomerStatus.Gold:
                    DomainEvents.Raise(CustomerIsMadeGold(this));
                    break;
                ...
            }
        }
    }

Raise 方法会将事件存储在 Event Store 中。它还可以执行本地注册的事件处理程序。

于 2011-05-05T12:44:37.620 回答
4

听起来您需要一个服务层。服务层是位于前端或控制器与业务层或域模型之间的另一种抽象。它有点像应用程序中的 API。然后,您的控制器将只能访问您的服务层。

然后,与您的域模型交互成为您的服务层的责任

public Order GetOrderById(int id) {
  //...
  var order = orderRepository.get(id);
  //...
  return order;
}

public CreateOrder(Order order) {
  //...
  orderRepositroy.Add(order);
  if (orderRepository.Submitchanges()) {
    var email = emailManager.CreateNewOrderEmail(order);
    email.Send();
  }
  //...
}

通常以“经理”对象结束,例如OrderManager与订单交互和服务层以处理 POCO。

UI 是否适合引发这些事件?它知道发生了哪些事件,并且只有在服务层成功保存对象后才能触发它们。让控制器触发域事件似乎有些不对劲。

不会。如果添加了新操作并且开发人员不知道或忘记了应该发送电子邮件,您最终会遇到问题。

成功持久化后存储库是否应该触发事件?

不,存储库的职责是提供对数据访问的抽象,仅此而已

有一个更好的方法吗?也许只有在对象被持久化后才让我的域对象排队由服务层触发的事件?

是的,听起来这应该由您的服务层处理。

于 2011-05-04T16:16:04.913 回答
1

结果证明,解决方案是基于在NHibernate 会话对象上实现这些扩展方法。

我可能对问题的措辞有点不清楚。架构问题的全部原因是 NHibernate 对象始终处于相同状态,除非您手动取消代理它们并经历各种阴谋。这是我不想做的事情,以确定哪些属性已更改,因此要触发哪些事件。

在属性设置器中触发这些事件是行不通的,因为这些事件应该只在更改被持久化后触发,以避免在最终可能失败的操作上触发事件。

所以我所做的就是在我的存储库库中添加一些方法:

public bool IsDirtyEntity(T entity)
{
    // Use the extension method...
    return SessionFactory.GetCurrentSession().IsDirtyEntity(entity);
}

public bool IsDirtyEntityProperty(T entity, string propertyName)
{
    // Use the extension method...
    return SessionFactory.GetCurrentSession().IsDirtyProperty(entity, propertyName);
}

然后,在我的服务Save方法中,我可以做这样的事情(记住我在这里使用的是 NServiceBus,但如果你使用的是 Udi Dahan 的域事件静态类,它会类似地工作):

var pendingEvents = new List<IMessage>();
if (_repository.IsDirtyEntityProperty(order, "Status"))
    pendingEvents.Add(new OrderStatusChanged()); // In reality I'd set the properties of this event message object

_repository.Save(order);
_unitOfWork.Commit();

// If we get here then the save operation succeeded
foreach (var message in pendingEvents)
    Bus.Send(message);

因为在某些情况下Id,实体在保存之前可能不会设置(我使用的是Identity整数列),所以我可能必须在提交事务后运行以检索 Id 以填充我的事件对象中的属性。由于这是现有数据,我无法轻松切换到 hilo 类型的客户端分配 ID。

于 2011-05-06T13:31:49.950 回答
1

我认为,正如 David Glenn 所说,您应该拥有域服务。

我遇到的问题是,服务层最终不得不在保存修改后的版本之前提取一些对象的副本,以将新版本与旧版本进行比较,然后决定应该触发哪些事件。

您的域服务应包含明确说明您想对域实体执行什么操作的方法,例如:RegisterNewOrder、CreateNoteForOrder、ChangeOrderStatus 等。

public class OrderDomainService()
{
    public void ChangeOrderStatus(Order order, OrderStatus status)
    {
        try
        {
            order.ChangeStatus(status);
            using(IUnitOfWork unitOfWork = unitOfWorkFactory.Get())
            {
                IOrderRepository repository = unitOfWork.GetRepository<IOrderRepository>();
                repository.Save(order);
                unitOfWork.Commit();
            }
            DomainEvents.Publish<OrderStatusChnaged>(new OrderStatusChangedEvent(order, status));
        }

    }
}
于 2011-12-11T10:18:49.890 回答
0

如果您有发送到您的服务层的命令,那么拥有域事件会很有效。然后它可以根据命令更新实体,并在单个事务中引发相应的领域事​​件。

如果您在UI服务层之间移动实体本身,则很难(有时甚至不可能)确定发生了哪些领域事件,因为它们不是显式的,而是隐藏在实体的状态下。

于 2011-05-04T17:09:05.520 回答