Entity Framework无法通过SaveChanges实现级联软删除的原因及替代解决方案
Entity Framework无法通过SaveChanges实现级联软删除的原因及替代解决方案
我之前在项目里也踩过这个坑:想给EF实现软删除机制——就是给实体加IsDeleted标记,不直接从数据库物理删除记录。一开始的思路很直接:重写SaveChanges方法,把所有标记为EntityState.Deleted的实体状态改成Modified,同时设置IsDeleted = true和删除时间。
但很快就发现问题了:父实体软删成功了,可是那些和父实体设置了DeleteBehavior.Cascade级联删除的依赖实体,根本没跟着被软删!
为什么会出现这个问题?
EF原生的级联删除逻辑是和物理删除强绑定的。当你把实体的状态从Deleted改成Modified(实现软删)时,EF内部触发级联删除的机制根本不会启动——它只认EntityState.Deleted的物理删除操作,所以不会自动处理依赖实体的软删,得我们自己想办法拦截级联过程。
替代方案:实现递归级联软删(RecursiveCascadeSoftDelete)
为了解决这个问题,我在AppDbContext里实现了RecursiveCascadeSoftDelete方法,手动模拟EF原生的级联删除逻辑,递归遍历所有带DeleteBehavior.Cascade的导航属性,自动处理所有关联实体的软删,不用手动一个个找关联。
前置准备:BaseEntity基类
首先所有需要软删的实体都要继承这个基类,包含软删必备字段:
public abstract class BaseEntity { public bool IsDeleted { get; set; } public DateTime? DeleteDate { get; set; } public Guid? DeleteUserId { get; set; } // 可根据需求添加CreateDate、CreateUserId、UpdateDate等公共字段 }
AppDbContext核心实现
public class AppDbContext : DbContext { // 构造函数、DbSet定义等基础代码... // 重写SaveChanges,先处理软删逻辑再执行保存 public override int SaveChanges() { PrepareEntities(); return base.SaveChanges(); } // 批量处理所有待操作的实体 private void PrepareEntities() { var entities = ChangeTracker.Entries<BaseEntity>().ToList(); foreach (var entity in entities) { switch (entity.State) { case EntityState.Deleted: HandleSoftDelete(entity); break; case EntityState.Added: HandleCreate(entity); break; case EntityState.Modified: if (!entity.Property(nameof(BaseEntity.IsDeleted)).CurrentValue) { HandleUpdate(entity); } break; } } } // 单个实体的软删处理 private void HandleSoftDelete(EntityEntry<BaseEntity> entity) { // 将物理删除转为软删:修改状态为Modified,设置软删标记和时间 entity.State = EntityState.Modified; entity.Property(nameof(BaseEntity.IsDeleted)).CurrentValue = true; entity.Property(nameof(BaseEntity.DeleteDate)).CurrentValue = DateTime.UtcNow; SetUserId(entity.Entity, UserProperty.DeleteUserId); // 给实体设置删除人ID,可根据自身系统实现 // 递归处理所有关联的级联实体 RecursiveCascadeSoftDelete(entity.Entity); } // 核心:递归遍历所有级联导航属性,自动软删依赖实体 private void RecursiveCascadeSoftDelete(BaseEntity mainEntity) { var entityType = Model.FindEntityType(mainEntity.GetType()); if (entityType == null) return; // 遍历当前实体的所有导航属性 foreach (var navigation in entityType.GetNavigations()) { var foreignKey = navigation.ForeignKey; // 只处理设置了Cascade级联规则的导航属性 if (foreignKey.DeleteBehavior != DeleteBehavior.Cascade) continue; // 加载关联实体(如果还未加载到内存) var related = navigation.GetGetter().GetClrValue(mainEntity); if (related == null) { var entry = Entry(mainEntity); if (navigation.IsCollection) entry.Collection(navigation.Name).Load(); else entry.Reference(navigation.Name).Load(); related = navigation.GetGetter().GetClrValue(mainEntity); } // 处理集合类型的关联实体(比如一对多的多端) if (related is IEnumerable<BaseEntity> collection) { foreach (var dependent in collection) TrySoftDeleteAndRecurse(dependent); } // 处理单个实体类型的关联(比如一对一的依赖端) else if (related is BaseEntity dependent) { TrySoftDeleteAndRecurse(dependent); } } } // 单个依赖实体的软删处理,同时递归处理它的子实体 private void TrySoftDeleteAndRecurse(BaseEntity dependent) { var dependentEntry = ChangeTracker.Entries<BaseEntity>() .FirstOrDefault(e => e.Entity == dependent) ?? Entry(dependent); // 如果实体是未变化/游离状态,先标记为Deleted(触发软删逻辑) if (dependentEntry.State == EntityState.Unchanged || dependentEntry.State == EntityState.Detached) { dependentEntry.State = EntityState.Deleted; } // 确保实体处于待删除状态,再转为软删 if (dependentEntry.State != EntityState.Deleted) return; dependentEntry.State = EntityState.Modified; dependentEntry.Property(nameof(BaseEntity.IsDeleted)).CurrentValue = true; dependentEntry.Property(nameof(BaseEntity.DeleteDate)).CurrentValue = DateTime.UtcNow; SetUserId(dependentEntry.Entity, UserProperty.DeleteUserId); // 递归处理当前依赖实体的关联实体 RecursiveCascadeSoftDelete(dependentEntry.Entity); } // 新增实体的公共字段初始化(示例) private void HandleCreate(EntityEntry<BaseEntity> entity) { entity.Property(nameof(BaseEntity.IsDeleted)).CurrentValue = false; entity.Property(nameof(BaseEntity.CreateDate)).CurrentValue = DateTime.UtcNow; SetUserId(entity.Entity, UserProperty.CreateUserId); } // 更新实体的公共字段初始化(示例) private void HandleUpdate(EntityEntry<BaseEntity> entity) { entity.Property(nameof(BaseEntity.UpdateDate)).CurrentValue = DateTime.UtcNow; SetUserId(entity.Entity, UserProperty.UpdateUserId); } // 根据操作类型设置用户ID(示例,需结合自身系统的用户上下文实现) private void SetUserId(BaseEntity entity, UserProperty property) { // 示例逻辑:从当前登录上下文获取用户ID // var userId = _currentUserService.GetCurrentUserId(); // switch (property) // { // case UserProperty.CreateUserId: // entity.CreateUserId = userId; // break; // case UserProperty.UpdateUserId: // entity.UpdateUserId = userId; // break; // case UserProperty.DeleteUserId: // entity.DeleteUserId = userId; // break; // } } public enum UserProperty { CreateUserId, UpdateUserId, DeleteUserId } }
方案核心优势
- 完全模拟原生级联:不需要手动遍历实体的每一层关联关系,只要导航属性设置了
DeleteBehavior.Cascade,就会自动递归软删所有依赖实体; - 侵入性低:所有实体只要继承
BaseEntity就能自动支持,不用额外配置; - 和EF生命周期兼容:完全集成在
SaveChanges的生命周期里,业务代码只要正常调用Remove方法标记删除,底层自动转为软删。
亲测这个方案能完美解决软删时的级联问题,现在父实体软删后,所有依赖的子实体都会跟着被软删,和物理删除的级联效果一模一样!
内容来源于stack exchange




