You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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
    }
}

方案核心优势

  1. 完全模拟原生级联:不需要手动遍历实体的每一层关联关系,只要导航属性设置了DeleteBehavior.Cascade,就会自动递归软删所有依赖实体;
  2. 侵入性低:所有实体只要继承BaseEntity就能自动支持,不用额外配置;
  3. 和EF生命周期兼容:完全集成在SaveChanges的生命周期里,业务代码只要正常调用Remove方法标记删除,底层自动转为软删。

亲测这个方案能完美解决软删时的级联问题,现在父实体软删后,所有依赖的子实体都会跟着被软删,和物理删除的级联效果一模一样!

内容来源于stack exchange

火山引擎 最新活动