领域事件
领域事件(Domain Event)是领域驱动设计中捕获业务事实的重要模式。它用于记录领域中发生的重要业务事件,实现聚合之间的松耦合通信。
什么是领域事件
Section titled “什么是领域事件”领域事件表示在领域中已经发生的事情,具有以下特征:
- 业务意义:反映真实的业务事实
- 过去时态:事件名称使用过去时(如
OrderPlaced,而非PlaceOrder) - 不可变性:事件一旦创建就不能修改
- 异步处理:事件处理器异步响应事件
定义领域事件
Section titled “定义领域事件”using MiCake.DDD.Domain;
// 订单已提交事件public class OrderSubmittedEvent : IDomainEvent{ public int OrderId { get; } public int CustomerId { get; } public decimal TotalAmount { get; } public DateTime SubmittedAt { get; }
public OrderSubmittedEvent(int orderId, int customerId, decimal totalAmount) { OrderId = orderId; CustomerId = customerId; TotalAmount = totalAmount; SubmittedAt = DateTime.UtcNow; }}
// 用户注册事件public class UserRegisteredEvent : IDomainEvent{ public int UserId { get; } public string Email { get; } public DateTime RegisteredAt { get; }
public UserRegisteredEvent(int userId, string email) { UserId = userId; Email = email; RegisteredAt = DateTime.UtcNow; }}Record 事件(推荐)
Section titled “Record 事件(推荐)”使用 C# record 可以更简洁地定义事件:
// 使用 record 定义事件public record ProductCreatedEvent(int ProductId, string Name, decimal Price) : IDomainEvent;
public record PriceChangedEvent(int ProductId, decimal OldPrice, decimal NewPrice) : IDomainEvent;
public record OrderCancelledEvent(int OrderId, string Reason) : IDomainEvent;触发领域事件
Section titled “触发领域事件”在聚合根中触发
Section titled “在聚合根中触发”public class Order : AggregateRoot<int>{ private List<OrderItem> _items = new();
public int CustomerId { get; private set; } public OrderStatus Status { get; private set; }
public void Submit() { if (Status != OrderStatus.Draft) throw new DomainException("Only draft orders can be submitted");
if (!_items.Any()) throw new DomainException("Cannot submit empty order");
// 修改状态 Status = OrderStatus.Submitted;
// 触发领域事件 RaiseDomainEvent(new OrderSubmittedEvent(Id, CustomerId, TotalAmount)); }
public void Cancel(string reason) { if (Status == OrderStatus.Shipped) throw new DomainException("Cannot cancel shipped order");
Status = OrderStatus.Cancelled; RaiseDomainEvent(new OrderCancelledEvent(Id, reason)); }
public void AddItem(int productId, int quantity, decimal price) { var item = new OrderItem(productId, quantity, price); _items.Add(item);
// 添加商品也可以触发事件 RaiseDomainEvent(new OrderItemAddedEvent(Id, productId, quantity)); }}处理领域事件
Section titled “处理领域事件”创建事件处理器
Section titled “创建事件处理器”using MiCake.DDD.Domain;using System.Threading;using System.Threading.Tasks;
// 订单提交事件处理器public class OrderSubmittedEventHandler : IDomainEventHandler<OrderSubmittedEvent>{ private readonly IEmailService _emailService; private readonly ILogger<OrderSubmittedEventHandler> _logger;
public OrderSubmittedEventHandler( IEmailService emailService, ILogger<OrderSubmittedEventHandler> logger) { _emailService = emailService; _logger = logger; }
public async Task HandleAysnc(OrderSubmittedEvent domainEvent, CancellationToken cancellationToken = default) { _logger.LogInformation($"Order {domainEvent.OrderId} submitted by customer {domainEvent.CustomerId}");
// 发送订单确认邮件 await _emailService.SendOrderConfirmationAsync( domainEvent.CustomerId, domainEvent.OrderId, domainEvent.TotalAmount );
// 其他业务逻辑... }}
// 用户注册事件处理器public class UserRegisteredEventHandler : IDomainEventHandler<UserRegisteredEvent>{ private readonly IEmailService _emailService; private readonly IRepository<UserProfile, int> _profileRepository;
public async Task HandleAysnc(UserRegisteredEvent domainEvent, CancellationToken cancellationToken = default) { // 1. 发送欢迎邮件 await _emailService.SendWelcomeEmailAsync(domainEvent.Email);
// 2. 创建用户档案 var profile = UserProfile.Create(domainEvent.UserId); await _profileRepository.AddAsync(profile, cancellationToken); await _profileRepository.SaveChangesAsync(cancellationToken);
// 3. 记录日志 Console.WriteLine($"User {domainEvent.UserId} registered at {domainEvent.RegisteredAt}"); }}一个事件多个处理器
Section titled “一个事件多个处理器”一个事件可以有多个处理器:
// 处理器 1:发送邮件public class OrderSubmittedEmailHandler : IDomainEventHandler<OrderSubmittedEvent>{ public async Task HandleAysnc(OrderSubmittedEvent domainEvent, CancellationToken cancellationToken) { // 发送邮件 }}
// 处理器 2:更新库存public class OrderSubmittedInventoryHandler : IDomainEventHandler<OrderSubmittedEvent>{ public async Task HandleAysnc(OrderSubmittedEvent domainEvent, CancellationToken cancellationToken) { // 扣减库存 }}
// 处理器 3:记录日志public class OrderSubmittedLoggingHandler : IDomainEventHandler<OrderSubmittedEvent>{ public async Task HandleAysnc(OrderSubmittedEvent domainEvent, CancellationToken cancellationToken) { // 记录日志 }}
// 这三个处理器会依次执行事件的自动派发
Section titled “事件的自动派发”MiCake 在调用 SaveChangesAsync() 时自动派发领域事件:
public class OrderService{ private readonly IRepository<Order, int> _orderRepository;
public async Task SubmitOrder(int orderId) { // 1. 加载聚合根 var order = await _orderRepository.FindAsync(orderId);
// 2. 调用业务方法(触发事件,但不立即派发) order.Submit(); // 内部:RaiseDomainEvent(new OrderSubmittedEvent(...))
// 3. 更新聚合根 await _orderRepository.UpdateAsync(order);
// 4. 保存更改 - 此时自动派发所有事件 await _orderRepository.SaveChangesAsync(); // SaveChangesAsync 内部流程: // a. 收集聚合根上的所有待处理事件 // b. 持久化数据到数据库 // c. 按顺序派发事件到对应的处理器 // d. 清除已派发的事件 }}事件派发流程
Section titled “事件派发流程”1. 业务方法调用 order.Submit() ↓2. 触发领域事件 RaiseDomainEvent(new OrderSubmittedEvent(...)) ↓3. 事件暂存在聚合根 _domainEvents.Add(event) ↓4. 保存更改 await repository.SaveChangesAsync() ↓5. 收集所有事件 events = aggregateRoot.DomainEvents ↓6. 持久化数据 dbContext.SaveChanges() ↓7. 派发事件 foreach (event in events) foreach (handler in GetHandlers(event)) await handler.HandleAsync(event) ↓8. 清除事件 aggregateRoot.ClearDomainEvents()1. 跨聚合通信
Section titled “1. 跨聚合通信”// 订单聚合public class Order : AggregateRoot<int>{ public void Submit() { Status = OrderStatus.Submitted;
// 触发事件,通知其他聚合 RaiseDomainEvent(new OrderSubmittedEvent(Id, Items)); }}
// 库存聚合在事件处理器中响应public class OrderSubmittedInventoryHandler : IDomainEventHandler<OrderSubmittedEvent>{ private readonly IRepository<Product, int> _productRepository;
public async Task HandleAysnc(OrderSubmittedEvent domainEvent, CancellationToken cancellationToken) { // 扣减库存 foreach (var item in domainEvent.Items) { var product = await _productRepository.FindAsync(item.ProductId); product.DecreaseStock(item.Quantity); await _productRepository.UpdateAsync(product); }
await _productRepository.SaveChangesAsync(cancellationToken); }}2. 业务流程协调
Section titled “2. 业务流程协调”// 用户注册流程public class User : AggregateRoot<int>{ public void Register(string email, string password) { // 注册逻辑 Email = email; SetPassword(password); Status = UserStatus.Pending;
// 触发注册事件 RaiseDomainEvent(new UserRegisteredEvent(Id, email)); }}
// 多个处理器协调完成注册流程public class SendVerificationEmailHandler : IDomainEventHandler<UserRegisteredEvent>{ public async Task HandleAysnc(UserRegisteredEvent domainEvent, CancellationToken cancellationToken) { // 发送验证邮件 }}
public class CreateUserProfileHandler : IDomainEventHandler<UserRegisteredEvent>{ public async Task HandleAysnc(UserRegisteredEvent domainEvent, CancellationToken cancellationToken) { // 创建用户档案 }}
public class InitializeUserSettingsHandler : IDomainEventHandler<UserRegisteredEvent>{ public async Task HandleAysnc(UserRegisteredEvent domainEvent, CancellationToken cancellationToken) { // 初始化用户设置 }}3. 审计和日志
Section titled “3. 审计和日志”public class OrderStatusChangedEvent : IDomainEvent{ public int OrderId { get; } public OrderStatus OldStatus { get; } public OrderStatus NewStatus { get; } public DateTime ChangedAt { get; }}
public class OrderAuditEventHandler : IDomainEventHandler<OrderStatusChangedEvent>{ private readonly IAuditLogRepository _auditRepository;
public async Task HandleAysnc(OrderStatusChangedEvent domainEvent, CancellationToken cancellationToken) { var auditLog = new AuditLog { EntityType = nameof(Order), EntityId = domainEvent.OrderId, Action = "StatusChanged", OldValue = domainEvent.OldStatus.ToString(), NewValue = domainEvent.NewStatus.ToString(), Timestamp = domainEvent.ChangedAt };
await _auditRepository.AddAsync(auditLog); await _auditRepository.SaveChangesAsync(cancellationToken); }}4. 发送通知
Section titled “4. 发送通知”public class OrderShippedEvent : IDomainEvent{ public int OrderId { get; } public int CustomerId { get; } public string TrackingNumber { get; }}
public class OrderShippedNotificationHandler : IDomainEventHandler<OrderShippedEvent>{ private readonly INotificationService _notificationService;
public async Task HandleAysnc(OrderShippedEvent domainEvent, CancellationToken cancellationToken) { // 发送邮件通知 await _notificationService.SendEmailAsync( domainEvent.CustomerId, "Order Shipped", $"Your order has been shipped. Tracking number: {domainEvent.TrackingNumber}" );
// 发送短信通知 await _notificationService.SendSmsAsync( domainEvent.CustomerId, $"Order shipped. Track: {domainEvent.TrackingNumber}" );
// 推送通知 await _notificationService.SendPushNotificationAsync( domainEvent.CustomerId, "Order Shipped", "Your order is on the way!" ); }}1. 事件命名使用过去时
Section titled “1. 事件命名使用过去时”// ✅ 正确 - 使用过去时public class OrderPlacedEvent : IDomainEvent { }public class PaymentCompletedEvent : IDomainEvent { }public class UserRegisteredEvent : IDomainEvent { }
// ❌ 错误 - 使用现在时或命令式public class PlaceOrderEvent : IDomainEvent { }public class CompletePaymentEvent : IDomainEvent { }public class RegisterUserEvent : IDomainEvent { }2. 事件应该是不可变的
Section titled “2. 事件应该是不可变的”// ✅ 正确 - 所有属性只读public class OrderCreatedEvent : IDomainEvent{ public int OrderId { get; } // 只读 public DateTime CreatedAt { get; }
public OrderCreatedEvent(int orderId) { OrderId = orderId; CreatedAt = DateTime.UtcNow; }}
// ❌ 错误 - 属性可修改public class OrderCreatedEvent : IDomainEvent{ public int OrderId { get; set; } // 可修改 public DateTime CreatedAt { get; set; }}3. 事件处理器保持幂等性
Section titled “3. 事件处理器保持幂等性”public class OrderCreatedEmailHandler : IDomainEventHandler<OrderCreatedEvent>{ private readonly IEmailService _emailService; private readonly IEmailLogRepository _emailLogRepository;
public async Task HandleAysnc(OrderCreatedEvent domainEvent, CancellationToken cancellationToken) { // 检查是否已发送(幂等性) var alreadySent = await _emailLogRepository.ExistsAsync( l => l.OrderId == domainEvent.OrderId && l.Type == "OrderCreated" );
if (alreadySent) return; // 已发送,跳过
// 发送邮件 await _emailService.SendOrderConfirmationAsync(domainEvent.OrderId);
// 记录日志 await _emailLogRepository.AddAsync(new EmailLog { OrderId = domainEvent.OrderId, Type = "OrderCreated", SentAt = DateTime.UtcNow });
await _emailLogRepository.SaveChangesAsync(cancellationToken); }}4. 事件包含足够的信息
Section titled “4. 事件包含足够的信息”// ✅ 好的做法 - 包含必要信息public class OrderSubmittedEvent : IDomainEvent{ public int OrderId { get; } public int CustomerId { get; } public decimal TotalAmount { get; } public List<OrderItemDto> Items { get; } // 包含详细信息 public DateTime SubmittedAt { get; }
// 事件处理器不需要再查询订单详情}
// ❌ 不好的做法 - 信息不足public class OrderSubmittedEvent : IDomainEvent{ public int OrderId { get; } // 只有 ID
// 事件处理器需要查询数据库获取详情}5. 避免在事件处理器中执行长时间操作
Section titled “5. 避免在事件处理器中执行长时间操作”// ❌ 避免 - 同步执行耗时操作public class OrderPlacedHandler : IDomainEventHandler<OrderPlacedEvent>{ public async Task HandleAysnc(OrderPlacedEvent domainEvent, CancellationToken cancellationToken) { // 这会阻塞事务 await SendEmailAsync(); // 可能很慢 await CallExternalApiAsync(); // 可能失败 await GeneratePdfAsync(); // 很耗时 }}
// ✅ 推荐 - 发布到消息队列异步处理public class OrderPlacedHandler : IDomainEventHandler<OrderPlacedEvent>{ private readonly IMessageQueue _messageQueue;
public async Task HandleAysnc(OrderPlacedEvent domainEvent, CancellationToken cancellationToken) { // 快速发布到队列 await _messageQueue.PublishAsync(new SendOrderEmailCommand(domainEvent.OrderId)); await _messageQueue.PublishAsync(new GenerateInvoiceCommand(domainEvent.OrderId)); }}MiCake 的领域事件机制:
- 实现
IDomainEvent接口定义事件 - 在聚合根中通过
RaiseDomainEvent触发事件 - 实现
IDomainEventHandler<TEvent>处理事件 - 在
SaveChangesAsync时自动派发事件 - 用于实现聚合间松耦合通信
- 支持一个事件多个处理器
下一步: