--- title: 领域事件 description: 使用领域事件实现事件驱动架构和跨聚合通信 --- 领域事件(Domain Event)是领域驱动设计中捕获业务事实的重要模式。它用于记录领域中发生的重要业务事件,实现聚合之间的松耦合通信。 ## 什么是领域事件 领域事件表示在领域中已经发生的事情,具有以下特征: 1. **业务意义**:反映真实的业务事实 2. **过去时态**:事件名称使用过去时(如 `OrderPlaced`,而非 `PlaceOrder`) 3. **不可变性**:事件一旦创建就不能修改 4. **异步处理**:事件处理器异步响应事件 ## 定义领域事件 ### 基本事件 ```csharp 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 事件(推荐) 使用 C# record 可以更简洁地定义事件: ```csharp // 使用 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; ``` ## 触发领域事件 ### 在聚合根中触发 ```csharp public class Order : AggregateRoot { private List _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)); } } ``` ## 处理领域事件 ### 创建事件处理器 ```csharp using MiCake.DDD.Domain; using System.Threading; using System.Threading.Tasks; // 订单提交事件处理器 public class OrderSubmittedEventHandler : IDomainEventHandler { private readonly IEmailService _emailService; private readonly ILogger _logger; public OrderSubmittedEventHandler( IEmailService emailService, ILogger logger) { _emailService = emailService; _logger = logger; } public async Task HandleAsync(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 { private readonly IEmailService _emailService; private readonly IRepository _profileRepository; public async Task HandleAsync(UserRegisteredEvent domainEvent, CancellationToken cancellationToken = default) { // 1. 发送欢迎邮件 await _emailService.SendWelcomeEmailAsync(domainEvent.Email); // 2. 创建用户档案 var profile = UserProfile.Create(domainEvent.UserId); await _profileRepository.AddAsync(profile, cancellationToken); // 变更由环境 UoW 统一提交,无需手动保存 // 3. 记录日志 Console.WriteLine($"User {domainEvent.UserId} registered at {domainEvent.RegisteredAt}"); } } ``` ### 一个事件多个处理器 一个事件可以有多个处理器: ```csharp // 处理器 1:发送邮件 public class OrderSubmittedEmailHandler : IDomainEventHandler { public async Task HandleAsync(OrderSubmittedEvent domainEvent, CancellationToken cancellationToken) { // 发送邮件 } } // 处理器 2:更新库存 public class OrderSubmittedInventoryHandler : IDomainEventHandler { public async Task HandleAsync(OrderSubmittedEvent domainEvent, CancellationToken cancellationToken) { // 扣减库存 } } // 处理器 3:记录日志 public class OrderSubmittedLoggingHandler : IDomainEventHandler { public async Task HandleAsync(OrderSubmittedEvent domainEvent, CancellationToken cancellationToken) { // 记录日志 } } // 这三个处理器会依次执行 ``` ## 事件的自动派发 MiCake 在**工作单元提交**(`IUnitOfWork.CommitAsync()`)时自动派发领域事件: ```csharp public class OrderService { private readonly IRepository _orderRepository; private readonly IUnitOfWork _unitOfWork; 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 _unitOfWork.CommitAsync(); // CommitAsync 内部流程: // a. 收集聚合根上的所有待处理事件 // b. 持久化数据到数据库(flush) // c. 按顺序派发事件到对应的处理器 // d. 清除已派发的事件 } } ``` ## 事件派发流程 ``` 1. 业务方法调用 order.Submit() ↓ 2. 触发领域事件 RaiseDomainEvent(new OrderSubmittedEvent(...)) ↓ 3. 事件暂存在聚合根 _domainEvents.Add(event) ↓ 4. 提交工作单元 await unitOfWork.CommitAsync() ↓ 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. 跨聚合通信 ```csharp // 订单聚合 public class Order : AggregateRoot { public void Submit() { Status = OrderStatus.Submitted; // 触发事件,通知其他聚合 RaiseDomainEvent(new OrderSubmittedEvent(Id, Items)); } } // 库存聚合在事件处理器中响应 public class OrderSubmittedInventoryHandler : IDomainEventHandler { private readonly IRepository _productRepository; public async Task HandleAsync(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); } // 变更由环境 UoW 统一提交 } } ``` ### 2. 业务流程协调 ```csharp // 用户注册流程 public class User : AggregateRoot { public void Register(string email, string password) { // 注册逻辑 Email = email; SetPassword(password); Status = UserStatus.Pending; // 触发注册事件 RaiseDomainEvent(new UserRegisteredEvent(Id, email)); } } // 多个处理器协调完成注册流程 public class SendVerificationEmailHandler : IDomainEventHandler { public async Task HandleAsync(UserRegisteredEvent domainEvent, CancellationToken cancellationToken) { // 发送验证邮件 } } public class CreateUserProfileHandler : IDomainEventHandler { public async Task HandleAsync(UserRegisteredEvent domainEvent, CancellationToken cancellationToken) { // 创建用户档案 } } public class InitializeUserSettingsHandler : IDomainEventHandler { public async Task HandleAsync(UserRegisteredEvent domainEvent, CancellationToken cancellationToken) { // 初始化用户设置 } } ``` ### 3. 审计和日志 ```csharp public class OrderStatusChangedEvent : IDomainEvent { public int OrderId { get; } public OrderStatus OldStatus { get; } public OrderStatus NewStatus { get; } public DateTime ChangedAt { get; } } public class OrderAuditEventHandler : IDomainEventHandler { private readonly IAuditLogRepository _auditRepository; public async Task HandleAsync(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); // 变更由环境 UoW 统一提交,无需手动保存 } } ``` ### 4. 发送通知 ```csharp public class OrderShippedEvent : IDomainEvent { public int OrderId { get; } public int CustomerId { get; } public string TrackingNumber { get; } } public class OrderShippedNotificationHandler : IDomainEventHandler { private readonly INotificationService _notificationService; public async Task HandleAsync(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. 事件命名使用过去时 ```csharp // ✅ 正确 - 使用过去时 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. 事件应该是不可变的 ```csharp // ✅ 正确 - 所有属性只读 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. 事件处理器保持幂等性 ```csharp public class OrderCreatedEmailHandler : IDomainEventHandler { private readonly IEmailService _emailService; private readonly IEmailLogRepository _emailLogRepository; public async Task HandleAsync(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 }); // 变更由环境 UoW 统一提交,无需手动保存 } } ``` ### 4. 事件包含足够的信息 ```csharp // ✅ 好的做法 - 包含必要信息 public class OrderSubmittedEvent : IDomainEvent { public int OrderId { get; } public int CustomerId { get; } public decimal TotalAmount { get; } public List Items { get; } // 包含详细信息 public DateTime SubmittedAt { get; } // 事件处理器不需要再查询订单详情 } // ❌ 不好的做法 - 信息不足 public class OrderSubmittedEvent : IDomainEvent { public int OrderId { get; } // 只有 ID // 事件处理器需要查询数据库获取详情 } ``` ### 5. 避免在事件处理器中执行长时间操作 ```csharp // ❌ 避免 - 同步执行耗时操作 public class OrderPlacedHandler : IDomainEventHandler { public async Task HandleAsync(OrderPlacedEvent domainEvent, CancellationToken cancellationToken) { // 这会阻塞事务 await SendEmailAsync(); // 可能很慢 await CallExternalApiAsync(); // 可能失败 await GeneratePdfAsync(); // 很耗时 } } // ✅ 推荐 - 发布到消息队列异步处理 public class OrderPlacedHandler : IDomainEventHandler { private readonly IMessageQueue _messageQueue; public async Task HandleAsync(OrderPlacedEvent domainEvent, CancellationToken cancellationToken) { // 快速发布到队列 await _messageQueue.PublishAsync(new SendOrderEmailCommand(domainEvent.OrderId)); await _messageQueue.PublishAsync(new GenerateInvoiceCommand(domainEvent.OrderId)); } } ``` ## 小结 MiCake 的领域事件机制: - 实现 `IDomainEvent` 接口定义事件 - 在聚合根中通过 `RaiseDomainEvent` 触发事件 - 实现 `IDomainEventHandler` 处理事件 - 在**工作单元提交**(`CommitAsync`/`FlushAsync`)时自动派发事件 - 用于实现聚合间松耦合通信 - 支持一个事件多个处理器 下一步: - 学习[领域服务](/domain-driven/domain-service/)了解服务设计 - 阅读[工作单元](/domain-driven/unit-of-work/)理解事务管理 - 查看[聚合根](/domain-driven/aggregate-root/)回顾聚合设计