我的ASP.NET MVC3 / NHibernate应用程序需要触发和处理与我的域对象相关的各种事件。例如,Order
对象可能包含OrderStatusChanged
或NoteCreatedForOrder
等事件。在大多数情况下,这些事件会导致发送电子邮件,因此我不能将它们留在MVC应用程序中。
我已经阅读了Udi Dahan的Domain Events和其他许多关于如何做这类事情的想法,我决定使用一个处理事件消息的基于NServiceBus的主机。我做了一些概念验证测试,这似乎运作良好。
我的问题是应该实际引发事件的应用层。我不想在有问题的对象成功保留之前触发事件(如果持久性失败,则无法发送创建注释的电子邮件)。
另一个问题是,在某些情况下,事件与聚合根目录下的对象相关联。在上面的示例中,通过将Note
添加到Order.Notes
集合并保存订单来保存Order
。这带来了一个问题,因为它很难评估在保存Event
时应该触发哪些事件。我想避免在保存更新的副本之前拉出对象的当前副本并查找差异。
UI是否适合举起这些活动?它知道发生了什么事件,并且只有在成功使服务层保存对象后才能触发它们。让控制器触发域事件似乎是错误的。
成功保存后,存储库是否应触发事件?
我是否应该完全分离事件,让存储库存储一个EventQueue
对象,然后由轮询器服务选择该对象,然后转换为NServiceBus的事件(或直接从轮询服务处理)?
有更好的方法吗?也许让我的域对象排队只有在持久化对象后服务层触发的事件?
更新:我有一个服务层,但是让它通过比较过程以确定在保存给定聚合根时应该触发哪些事件似乎很麻烦且过分。由于其中一些事件是粒状的(例如“订单状态已更改”),我认为我必须检索对象的数据库副本,比较属性以创建事件,保存新对象,然后将事件发送到NServiceBus保存操作成功完成。
在我下面发布的答案(way below)之后,我最终做的是在我的域实体中构建一个List<IDomainEvent>
属性Guid.Comb
。然后我添加了事件,因为对域的更改是值得的,这使得我可以将逻辑保留在域中,我认为这是合适的,因为我根据实体内发生的事件触发事件。
然后,当我将对象保留在服务层中时,我处理该队列并实际将事件发送到服务总线。最初,我计划使用使用身份PK的遗留数据库,因此我必须对这些事件进行后处理以填充实体的ID,但我最终决定切换到{{1}} PK,这允许我跳过那一步。
答案 0 :(得分:9)
我的解决方案是在域层和服务层中引发事件。
您的域名:
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);
}
}
}
答案 1 :(得分: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中。它还可以执行本地注册的事件处理程序。
答案 2 :(得分:3)
听起来你需要Service Layer。服务层是位于前端或控制器与业务层或域模型之间的另一个抽象。它有点像你的应用程序的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是否适合举起这些事件?它知道发生了什么事件,并且只有在成功使服务层保存对象后才能触发它们。让控制器触发域事件似乎是错误的。
没有。如果添加新操作并且开发人员不知道或忘记发送电子邮件,您将会遇到问题。
成功保存后,存储库是否应触发事件?
没有。存储库的责任是提供数据访问的抽象而不是其他任何东西
有更好的方法吗?也许让我的域对象排队只有在持久化对象后才由服务层触发的事件?
是的,听起来这应该由您的服务层处理。
答案 3 :(得分:1)
该解决方案基于在NHibernate会话对象上实现these extension methods。
我对这个问题的措辞可能有点不清楚。架构问题的全部原因是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。
答案 4 :(得分:0)
如果您有命令,则将域事件发送到您的服务层。然后,它可以根据命令更新实体,并在单个事务中引发相应的域事件。
如果您要在 UI 和服务层之间移动实体,那么确定发生了哪些域事件变得非常困难(有时甚至是不可能的),因为它们是没有明确,但隐藏在实体的状态下。
答案 5 :(得分:0)
我认为,正如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));
}
}
}