重写SaveChanges实现实体版本化拷贝时类型转换异常问题咨询
问题根因
你调用SerializeDeserialize方法时传入的modifiedEntry.Entity是object类型,泛型方法的类型参数T会被推断为object,导致反序列化返回的结果是object实例,无法正确转换为IVersionedEntity接口,因此抛出转换异常。
你提出的IVersionable方案可行性
该方案完全可行,且是比序列化克隆更优的实现方案,优势如下:
- 直接调用
modifiedEntry.OriginalValues.ToObject()获取EF跟踪的实体修改前的原始值,没有序列化/反序列化的性能开销 - 天然避免了实体导航属性循环引用导致序列化失败的问题
- 所有需要留存变更记录的实体统一实现同一个接口,后续新增版本实体不需要修改
SaveChangesAsync的核心逻辑
完整实现代码
首先定义通用接口:
public interface IVersionable { int Id { get; set; } int? ParentVersionId { get; set; } }
需要留存版本的实体实现该接口:
public class DataClass : IVersionable { public int Id { get; set; } public string Adi { get; set; } public int? ParentVersionId { get; set; } public virtual DataClass? ParentVersion { get; set; } public virtual ICollection<DataClass> ParentVersions { get; set; } }
重写SaveChangesAsync方法:
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { // 只筛选已修改且实现了IVersionable接口的实体 var modifiedEntries = ChangeTracker.Entries() .Where(e => e.State == EntityState.Modified && e.Entity is IVersionable) .ToList(); foreach (var modifiedEntry in modifiedEntries) { // 从原始值创建旧版本实体实例 var oldVersion = (IVersionable)modifiedEntry.OriginalValues.ToObject(); // 重置主键为0,让EF识别为新插入实体 oldVersion.Id = 0; // 关联父版本Id为当前修改实体的原有主键 oldVersion.ParentVersionId = ((IVersionable)modifiedEntry.Entity).Id; // 添加到上下文 Add(oldVersion); } return base.SaveChangesAsync(cancellationToken); }
补充:坚持使用序列化克隆的解决方法
如果你的场景需要连带导航属性一起快照,必须使用序列化克隆的话,可以通过动态类型运行时推断泛型参数:
dynamic entity = modifiedEntry.Entity; var clonedEntity = (IVersionable)ObjectUtility.SerializeDeserialize(entity);
注意序列化配置需要添加ReferenceLoopHandling = ReferenceLoopHandling.Ignore,避免实体循环引用导致序列化失败。
注意事项
- 你原有代码中存在笔误:
cloned.ParentVersionId应改为clonedEntity.ParentVersionId,否则会抛出空引用异常 - 如果需要留存新增实体的首个版本,可以额外筛选
EntityState.Added状态的实体做对应处理
内容的提问来源于stack exchange,提问作者serkanz
相关产品推荐
相关产品推荐

