EF Core中重写SaveChanges时,如何在不额外查询数据库的情况下获取包含导航属性的完整前后对象用于审计
EF Core中重写SaveChanges时,如何在不额外查询数据库的情况下获取包含导航属性的完整前后对象用于审计
我之前做EF Core审计日志模块的时候也踩过这个坑,太懂这种无奈了——OriginalValues只能拿到简单标量属性的原始值,复杂类型、导航属性或者集合的原始状态完全拿不到,又不想额外查库影响性能。
咱们先搞清楚根因:EF Core的变更追踪系统默认只对标量属性做原始值快照,导航属性的原始状态不会自动保存,毕竟要是所有关联对象都自动快照,内存开销会非常大,这是框架的设计取舍。
那有没有办法不用查库就能拿到完整的原始对象?当然有,给你几个实操可行的方案:
方案一:提前通过ChangeTracker.Tracked事件创建实体快照
这是最靠谱的无查询方案,核心思路是在实体刚被上下文追踪且还没被修改的时候,手动创建一个深拷贝的快照存起来,等SaveChanges的时候再拿这个快照和当前实体对比。
具体步骤:
- 在你的DbContext里定义一个私有字典,用来存实体的快照,key是实体实例,value是深拷贝后的原始对象:
private readonly Dictionary<object, object> _entitySnapshots = new Dictionary<object, object>();
- 在DbContext的构造函数里订阅
ChangeTracker.Tracked事件,当实体从数据库查询出来并进入Unchanged状态时,创建深拷贝快照:
public YourDbContext(DbContextOptions<YourDbContext> options) : base(options) { ChangeTracker.Tracked += OnEntityTracked; } private void OnEntityTracked(object sender, EntityTrackedEventArgs e) { // 只对从数据库查询来的、处于Unchanged状态的实体创建快照 if (e.FromQuery && e.Entry.State == EntityState.Unchanged) { // 用Json序列化反序列化做深拷贝,也可以用AutoMapper等工具 var snapshotJson = JsonSerializer.Serialize(e.Entry.Entity); var snapshot = JsonSerializer.Deserialize(snapshotJson, e.Entry.Entity.GetType()); _entitySnapshots[e.Entry.Entity] = snapshot; } }
- 重写SaveChanges的时候,就可以从这个字典里取出原始快照,和当前实体对比了:
public override int SaveChanges(bool acceptAllChangesOnSuccess) { foreach (var entry in ChangeTracker.Entries().Where(e => e.State is EntityState.Modified or EntityState.Deleted)) { // 从快照字典里拿原始对象 if (_entitySnapshots.TryGetValue(entry.Entity, out var originalEntity)) { var currentEntity = entry.Entity; // 这里就可以对比originalEntity(原始状态,包含导航属性)和currentEntity(当前状态)了 // 执行你的审计逻辑,比如记录哪些导航属性的字段变了 } } var result = base.SaveChanges(acceptAllChangesOnSuccess); // 保存后,更新快照或者清理已修改/删除的实体快照,避免内存泄漏 foreach (var entry in ChangeTracker.Entries().Where(e => e.State is EntityState.Modified or EntityState.Deleted)) { _entitySnapshots.Remove(entry.Entity); // 如果是Modified状态,也可以重新创建新的快照,方便后续修改 if (entry.State == EntityState.Unchanged) { var snapshotJson = JsonSerializer.Serialize(entry.Entity); var snapshot = JsonSerializer.Deserialize(snapshotJson, entry.Entity.GetType()); _entitySnapshots[entry.Entity] = snapshot; } } return result; }
注意事项:
- 深拷贝的方式:Json序列化是最省心的,但如果实体有循环引用,要配置序列化选项(比如
ReferenceHandler.IgnoreCycles);也可以用AutoMapper把实体映射到新实例,性能会比Json好一点。 - 内存泄漏问题:记得在实体被修改、删除或者Detach的时候,从快照字典里移除对应的条目,不然字典会越来越大。
- 手动Attach的实体:如果你的实体是手动Attach到上下文的,不是从查询来的,要调整
OnEntityTracked的逻辑,比如判断e.Entry.State == EntityState.Unchanged的时候也创建快照。
方案二:遍历导航属性手动追踪(局限性较大)
如果你不想提前做全量快照,也可以尝试遍历实体的导航属性条目,但这个方案局限性比较大:
- 对于引用导航属性,你可以通过
entry.Navigations拿到所有导航属性的条目,但EF Core不会保存导航属性的原始值,所以只有当导航属性本身也被上下文追踪且处于Unchanged状态时,才能拿到原始对象;如果导航属性已经被修改,那拿到的也是修改后的值。 - 对于集合导航属性,这个方法基本没用,因为集合的变更(增删元素)EF Core不会记录原始集合的状态,还是得提前快照。
示例代码(仅供参考,实用性有限):
foreach (var navEntry in entry.Navigations) { if (navEntry is ReferenceEntry referenceEntry) { // 引用导航属性的当前值 var currentNavValue = referenceEntry.CurrentValue; // 这里无法直接拿到原始值,除非你提前存了快照 } }
避坑提醒
你之前尝试从上下文手动拿实体却拿到新值或者null,是因为当实体被修改后,上下文里的实例已经是修改后的状态了——EF Core的上下文是“工作单元”,一旦你修改了实体,上下文里的实例就会同步更新,所以必须在实体被修改之前保存好原始状态的快照,这是核心要点。
内容来源于stack exchange
相关产品推荐
相关产品推荐

