EF CORE 软删除

向应用添加软删除有如下步骤:

  1. 向需要软删除的实体类添加软删除属性

  2. 向 DbContext 中添加代码,以对这些实体类应用查询过滤器

  3. 如何设置/重置软删除

1.添加软删除属性

对于标准的软删除实现,你需要一个布尔标志来控制软删除,例如,这里有一个名叫 SoftDeleted 属性的 Book 实体。

public class Book : ISoftDelete
{
    public int BookId { get; set; }
    public string Title { get; set; }
    //... 其它无关属性

    public bool SoftDeleted { get; set; }
}

你可以通过它的名字SoftDeleted来区分软删除属性,如果它的值是true则该实体删除了,这意味着当你创建一个新实体时,它不会被软删除。

我还添加了一个 Book 类的ISoftDelete接口(第 1 行),这个接口表示该类必须有一个可以读写的公共SoftDeleted属性。这个接口将使得在DbContext中配置软删除查询过滤器变得更加容易。

2.配置查询过滤器

你必须告诉 EF Core 哪个实体类需要一个查询过滤器,该过滤器是查询表达式,用来把不需要被看到的实体过滤掉。你可以在 DbContext 中使用以下代码手动完成此操作。

public class EfCoreContext : DbContext
{
    public EfCoreContext(DbContextOptions<EfCoreContext> option)
        : base(options)
    {}

    protected override OnModelCreating(ModelBuilder modelBuilder)
    {
        // 省略其它和软删除无关的代码

        modelBuilder.Entity<Book>().HasQueryFilter(p => !p.SoftDeleted);
    }
}

这很好,但是让我向你展示一种自动添加查询过滤器的方法。

在 DbContext 中的 OnModelCreating 方法中,你可以通过 Fluent API 配置 EF Core。但是也有一种方法可以判断每个实体类并决定如何配置它。在下面的代码中,你可以看到 foreach 循环依次遍历每个实体类,检查实体类是否实现了 ISoftDelete 接口,如果实现了,它将调用我创建的扩展方法来应用正确的软删除过滤器配置。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 省略其它无关的代码

    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        // 省略其它无关的代码

        if (typeof(ISoftDelete).IsAssignableFrom(entityType.ClrType))
        {
            entityType.AddSoftDeleteQueryFilter();
        }
    }
}

有许多配置可以直接应用于 GetEntityTypes 方法返回的类型,但是设置查询过滤器需要更多的工作。这是因为查询过滤器中的 LINQ 查询需要实体类的类型来创建正确的 LINQ 表达式。为此,我创建了一个小型扩展类,它可以动态创建正确的 LINQ 表达式来配置查询过滤器。

public static class SoftDeleteQueryExtension
{
    public static void AddSoftDeleteQueryFilter(
        this IMutableEntityType entityData)
    {
        var methodToCall = typeof(SoftDeleteQueryExtension)
            .GetMethod(nameof(GetSoftDeleteFilter),
                BindingFlags.NonPublic | BindingFlags.Static)
            .MakeGenericMethod(entityData.ClrType);
        var filter = methodToCall.Invoke(null, new object[] { });
        entityData.SetQueryFilter((LambdaExpression)filter);
    }

    private static LambdaExpression GetSoftDeleteFilter<TEntity>()
        where TEntity : class, ISoftDelete
    {
        Expression<Func<TEntity, bool>> filter = x => !x.SoftDeleted;
        return filter;
    }
}

我真的很喜欢这个操作,因为它可以节省我的时间,也避免我忘记配置每一个查询过滤器。

3.如何设置/重置软删除

将“软删除”属性设置为 true 很容易,对应的场景是用户选择一个条目并单击(软)“删除”,这会返回实体的主键。用代码实现如下:

var entity = context.Books.Single(x => x.BookId == id);
entity.SoftDeleted = true;
context.SaveChanges();

重置软删除属性在实际的业务场景中有点复杂。首先,你很可能想要向用户显示一个已删除实体的列表——把它想象成显示某个实体类类型的实例回收站,例如 Book。要做到这一点,需要在你的查询中使用IgnoreQueryFilters方法,这意味着你将得到所有的实体(包括那些没有被软删除的和被软删除的),然后再根据需要选出那些 SoftDeleted 属性为 true 的。

var softDelEntities = _context.Books.IgnoreQueryFilters()
    .Where(x => x.SoftDeleted).ToList();

相应的,当你收到一个重设 SoftDeleted 属性的请求时(它通常包含实体类的主键),则要加载此条目时,需要在查询中使用IgnoreQueryFilters方法。

var entity = context.Books.IgnoreQueryFilters()
     .Single(x => x.BookId == id);
entity.SoftDeleted = false;
context.SaveChanges();

使用软删除注意事项

首先,需要说的是查询过滤器是非常安全的。我的意思是,如果查询过滤器返回 false,那么特定的实体/行将不会包含在查询(包括 Find 和 Include 等)返回的结果集中。你可以使用直接 SQL 绕过它,但除此之外,EF Core 会隐藏你软删除的数据。

但有几点你需要注意。

小心软删除过滤器与其它过滤器的混合使用

查询过滤器非常适合于软删除,但是查询过滤器更适合于控制对数据组的访问。例如,假设您想要构建一个 Web 应用程序来为多个公司提供服务,比如工资单。在这种情况下,你需要确保 A 公司看不到 B 公司的数据,反之亦然。这种类型的系统称为多租户应用程序,而查询过滤器非常适合此类场景。

问题是,每个实体类型只允许使用一个查询过滤器,因此,如果您想在多租户系统中使用软删除,那么您必须将这两个部分结合起来形成查询过滤器——下面是查询过滤器的示例:

modelBuilder.Entity<MyEntity>()
    .HasQueryFilter(x => !x.SoftDeleted
      && x.TenantId == currentTenantId);

这看上去很好,但是当你使用IgnoreQueryFilters方法忽略软删除标记进行查询时,它会忽略整个查询过滤器,包括多租户部分。因此,如果不小心,还会显示多租户数据!

答案是为自己构建一个特定于应用程序的IgnoreSoftDeleteFilter方法,如下所示:

public static IQueryable<TEntity> IgnoreSoftDeleteFilter<TEntity>(
    this IQueryable<TEntity> baseQuery, string currentTenantId)
    where TEntity : class, ITenantId
{
    return baseQuery.IgnoreQueryFilters()
        .Where(x => x.TenantId == currentTenantId)
}

这将忽略所有筛选器,然后把多租户筛选器添加回去。这将使它更容易更安全地处理显示/重置被软删除的实体。

更多的看这里吧:连接