基于EventSourcing的.NET应用聚合根事件变更内容存储方案咨询
基于聚合根的事件内容设计与EF Core实现方案
核心原则:聚焦聚合根边界内的精准变更
事件只需要记录聚合根标识、变更的具体对象/属性、变更后的值,不需要存储全量实体数据。比如修改Customer关联的Address某属性时,事件要包含:
- CustomerId(聚合根唯一标识)
- AddressId(关联子实体的唯一标识)
- 变更的属性名(如
Street) - 属性的新值
基于EF Core Change Tracker的实现步骤
1. 筛选聚合根及关联子实体的变更
在DbContext的SaveChanges方法中,遍历ChangeTracker,只处理聚合根实体(如Customer)和其边界内的子实体(如Address)的Modified状态条目。
2. 提取精准变更内容
对比实体的OriginalValues和CurrentValues,只提取真正发生变化的属性,避免冗余数据。对于子实体的变更,必须关联到所属的聚合根ID,确保事件归属清晰。
3. 定义强类型事件
不要用泛型或模糊的事件结构,针对不同变更场景创建强类型事件类,便于后续事件溯源和处理。
4. 原子性写入事件与数据库变更
确保数据库变更和事件写入在同一个事务内,避免数据不一致。
代码示例
强类型事件定义
// Address属性变更事件 public class CustomerAddressPropertyUpdatedEvent { public Guid CustomerId { get; set; } public Guid AddressId { get; set; } public string PropertyName { get; set; } public object NewValue { get; set; } public DateTime Timestamp { get; set; } = DateTime.UtcNow; } // Customer自身属性变更事件 public class CustomerPropertyUpdatedEvent { public Guid CustomerId { get; set; } public string PropertyName { get; set; } public object NewValue { get; set; } public DateTime Timestamp { get; set; } = DateTime.UtcNow; }
DbContext中处理变更并生成事件
public class AppDbContext : DbContext { private readonly IEventStore _eventStore; public AppDbContext(DbContextOptions<AppDbContext> options, IEventStore eventStore) : base(options) { _eventStore = eventStore; } public override int SaveChanges(bool acceptAllChangesOnSuccess) { var events = new List<object>(); // 处理Customer关联的Address变更 foreach (var entry in ChangeTracker.Entries<Address>() .Where(e => e.State == EntityState.Modified)) { var address = entry.Entity; // 获取所属的Customer聚合根 var customer = ChangeTracker.Entries<Customer>() .FirstOrDefault(c => c.Entity.Id == address.CustomerId)?.Entity; if (customer == null) continue; // 提取实际变更的属性 var changedProperties = entry.OriginalValues.Properties .Where(p => !entry.OriginalValues[p].Equals(entry.CurrentValues[p])) .Select(p => new { PropertyName = p.Name, NewValue = entry.CurrentValues[p] }); foreach (var prop in changedProperties) { events.Add(new CustomerAddressPropertyUpdatedEvent { CustomerId = customer.Id, AddressId = address.Id, PropertyName = prop.PropertyName, NewValue = prop.NewValue }); } } // 处理Customer自身的属性变更 foreach (var entry in ChangeTracker.Entries<Customer>() .Where(e => e.State == EntityState.Modified)) { var customer = entry.Entity; var changedProperties = entry.OriginalValues.Properties .Where(p => !entry.OriginalValues[p].Equals(entry.CurrentValues[p])) .Select(p => new { PropertyName = p.Name, NewValue = entry.CurrentValues[p] }); foreach (var prop in changedProperties) { events.Add(new CustomerPropertyUpdatedEvent { CustomerId = customer.Id, PropertyName = prop.PropertyName, NewValue = prop.NewValue }); } } // 开启事务保证数据库变更与事件写入原子性 using var transaction = Database.BeginTransaction(); try { var result = base.SaveChanges(acceptAllChangesOnSuccess); // 写入事件到EventStore foreach (var evnt in events) { _eventStore.AppendEvent(evnt); } transaction.Commit(); return result; } catch { transaction.Rollback(); throw; } } }
关键注意事项
- 聚合根边界清晰:确保子实体(如Address)无法脱离聚合根(Customer)单独存在,所有子实体的变更都归属于聚合根事件。
- 避免过度反射:反射仅用于对比属性值,事件类型必须是强类型,降低后续维护成本。
- 事件版本控制:如果后续事件结构需要调整,要添加版本号(如
CustomerAddressPropertyUpdatedEventV2),避免溯源时出现解析错误。 - 事件序列化:写入EventStore时,选择稳定的序列化方式(如JSON),确保事件可以长期解析。
内容的提问来源于stack exchange,提问作者William Peter
相关产品推荐
相关产品推荐

