EF Core: 使用阴影属性和查询过滤器进行软删除

14

我创建了一个接口,尝试使用影子属性和查询过滤器来进行软删除,但它没有起作用。

public interface IDeletableEntity {}

然后在我的模型构建器中

 builder.Model.GetEntityTypes()
                .Where(entityType => typeof(IDeletableEntity).IsAssignableFrom(entityType.ClrType))
                .ToList()
                .ForEach(entityType =>
                {
                    builder.Entity(entityType.ClrType).Property<Boolean>("IsDeleted");
                    builder.Entity(entityType.ClrType).HasQueryFilter(e => EF.Property<Boolean>(e, "IsDeleted") == false);
                });

但是带有查询过滤器的那一行无法编译。我得到的错误是“无法将 lambda 表达式转换为类型 'lambda 表达式',因为它不是一个委托类型”。

如果我这样做就可以工作。

builder.Entity<MyEntity>().HasQueryFilter(m => EF.Property<Boolean>(m, "IsDeleted") == false);

有没有办法做到这一点?这是为了拥有一个带有“IDeletableEntity”接口的界面,而不必在想要使用软删除实体的每个实体中都这样做。

非常感谢您提前的帮助!

8个回答

19

HasQueryFilter是非泛型的EntityTypeBuilder的方法(与泛型的EntityTypeBuilder<TEntity>相对),因为没有简单的方法来创建期望的LambdaExpression,所以几乎无法使用。

一种解决方案是手动使用Expression类的方法构建lambda表达式:

.ForEach(entityType =>
{
    builder.Entity(entityType.ClrType).Property<Boolean>("IsDeleted");
    var parameter = Expression.Parameter(entityType.ClrType, "e");
    var body = Expression.Equal(
        Expression.Call(typeof(EF), nameof(EF.Property), new[] { typeof(bool) }, parameter, Expression.Constant("IsDeleted")),
    Expression.Constant(false));
    builder.Entity(entityType.ClrType).HasQueryFilter(Expression.Lambda(body, parameter));
});

另一种方法是使用原型表达式。
Expression<Func<object, bool>> filter = 
    e => EF.Property<bool>(e, "IsDeleted") == false;

并使用参数替换器将参数与实际类型绑定:
.ForEach(entityType =>
{
    builder.Entity(entityType.ClrType).Property<Boolean>("IsDeleted");
    var parameter = Expression.Parameter(entityType.ClrType, "e");
    var body = filter.Body.ReplaceParameter(filter.Parameters[0], parameter);
    builder.Entity(entityType.ClrType).HasQueryFilter(Expression.Lambda(body, parameter));
});

其中ReplaceParameter是我用于表达式树操作的自定义辅助扩展方法之一:

public static partial class ExpressionUtils
{
    public static Expression ReplaceParameter(this Expression expr, ParameterExpression source, Expression target) =>
        new ParameterReplacer { Source = source, Target = target }.Visit(expr);

    class ParameterReplacer : System.Linq.Expressions.ExpressionVisitor
    {
        public ParameterExpression Source;
        public Expression Target;
        protected override Expression VisitParameter(ParameterExpression node) => node == Source ? Target : node;
    }
}

但我认为最自然的解决方法是将配置代码移动到通用方法中,并通过反射调用它。例如:

static void ConfigureSoftDelete<T>(ModelBuilder builder)
    where T : class, IDeletableEntity
{
    builder.Entity<T>().Property<Boolean>("IsDeleted");
    builder.Entity<T>().HasQueryFilter(e => EF.Property<bool>(e, "IsDeleted") == false);
}

然后

.ForEach(entityType => GetType()
    .GetMethod(nameof(ConfigureSoftDelete), BindingFlags.NonPublic | BindingFlags.Static)
    .MakeGenericMethod(entityType.ClrType)
    .Invoke(null, new object[] { builder })
);

2
谢谢,你在这里帮我省了很多麻烦。 - johnny 5
第二个代码块中的 filter 是什么? - Hassan Faghihi
@deadManN 很好的问题,看起来我忘记把它包括进去了。谢谢你指出来,帖子已更新。 - Ivan Stoev
感谢您,这个问题困扰了我很长时间(我的意思是整个答案),但仍然没有一种直接编译和使用Expression<Func<...>>(x=>x.IsDeleted)的方法,或者以某种接近的方式。 - Hassan Faghihi

18
我已经找到了一个简单的解决方案;-)。无论如何,还是谢谢Ivan Stoev。
界面是:
public interface IDeletableEntity
{
    bool IsDeleted { get; }
}

并且在你的模型构建器配置中:

builder.Model.GetEntityTypes()
                       .Where(entityType => typeof(IDeletableEntity).IsAssignableFrom(entityType.ClrType))
                       .ToList()
                       .ForEach(entityType =>
                       {
                           builder.Entity(entityType.ClrType)
                           .HasQueryFilter(ConvertFilterExpression<IDeletableEntity>(e => !e.IsDeleted, entityType.ClrType));
                       });

您需要转换过滤表达式。

private static LambdaExpression ConvertFilterExpression<TInterface>(
                            Expression<Func<TInterface, bool>> filterExpression,
                            Type entityType)
                {
                    var newParam = Expression.Parameter(entityType);
                    var newBody = ReplacingExpressionVisitor.Replace(filterExpression.Parameters.Single(), newParam, filterExpression.Body);

                    return Expression.Lambda(newBody, newParam);
                }

4
@SamazoOo的回答有一个小优化。你可以编写一个扩展方法,使其更加一致。
public static EntityTypeBuilder HasQueryFilter<T>(this EntityTypeBuilder entityTypeBuilder, Expression<Func<T, bool>> filterExpression)
    {
        var param = Expression.Parameter(entityTypeBuilder.Metadata.ClrType);
        var body = ReplacingExpressionVisitor.Replace(filterExpression.Parameters.Single(), param, filterExpression.Body);

        var lambdaExp = Expression.Lambda(body, param);

        return entityTypeBuilder.HasQueryFilter(lambdaExp);
    }

1

这对我不起作用,我的 .net core 版本是 3.1,所以我尝试了以下类似的方法:

// fetch entity types by reflection then: 

 softDeletedEntityTypes.ForEach(entityType =>
            {
                modelBuilder.Entity(entityType, builder =>
                {
                    builder.Property<bool>("IsDeleted");
                    builder.HasQueryFilter(GenerateQueryFilterExpression(entityType));
                });
            });


 private static LambdaExpression GenerateQueryFilterExpression(Type entityType)
        {            
             // the following lambda expression should be generated
             // e => !EF.Property<bool>(e, "IsDeleted")); 

            var parameter = Expression.Parameter(entityType, "e"); // e =>

            var fieldName = Expression.Constant("IsDeleted", typeof(string)); // "IsDeleted"

            // EF.Property<bool>(e, "IsDeleted")
            var genericMethodCall = Expression.Call(typeof(EF), "Property", new[] {typeof(bool)}, parameter, fieldName);

            // !EF.Property<bool>(e, "IsDeleted"))
            var not = Expression.Not(genericMethodCall);

            // e => !EF.Property<bool>(e, "IsDeleted"));
            var lambda = Expression.Lambda(not, parameter);
        }


0

对于EF core 6.0版本,这是一个扩展函数,它将“软删除”过滤器查询应用于所有扩展到IDeletableEntity接口的实体。

  1. 可为空版本 (bool? IsDeleted)
    public interface IDeletableEntity
    {
      public bool? IsDeleted { get; set; }
    }
      
      public static void UseSoftDelete(this ModelBuilder modelBuilder)
      {
        var softDeleteEntities = modelBuilder.Model
        .GetEntityTypes()
        .Where(t => t.ClrType.IsAssignableTo(typeof(IDeletableEntity)))
        .ToArray();
        foreach (var softDeleteEntity in softDeleteEntities)
        {
            var entityBuilder = modelBuilder.Entity(softDeleteEntity.ClrType);
            var parameter = Expression.Parameter(entityType, "e");
            var methodInfo = typeof(EF).GetMethod(nameof(EF.Property))!.MakeGenericMethod(typeof(bool?))!;
            var efPropertyCall = Expression.Call(null, methodInfo, parameter, Expression.Constant(nameof(IDeletableEntity.IsDeleted)));
            var converted = Expression.MakeBinary(ExpressionType.Coalesce, efPropertyCall, Expression.Constant(false));
            var body = Expression.MakeBinary(ExpressionType.Equal, converted, Expression.Constant(false));
            var expression = Expression.Lambda(body, parameter);
            entityBuilder.HasQueryFilter(expression);
        }
      }

非空版本 (bool IsDeleted)
    public interface IDeletableEntity
    {
      public bool IsDeleted { get; set; }
    }
      
      public static void UseSoftDelete(this ModelBuilder modelBuilder)
      {
        var softDeleteEntities = modelBuilder.Model
        .GetEntityTypes()
        .Where(t => t.ClrType.IsAssignableTo(typeof(IDeletableEntity)))
        .ToArray();
        foreach (var softDeleteEntity in softDeleteEntities)
        {
            var entityBuilder = modelBuilder.Entity(softDeleteEntity.ClrType);
            var parameter = Expression.Parameter(entityType, "e");
            var methodInfo = typeof(EF).GetMethod(nameof(EF.Property))!.MakeGenericMethod(typeof(bool))!;
            var efPropertyCall = Expression.Call(null, methodInfo, parameter, Expression.Constant(nameof(IDeletableEntity.IsDeleted)));
            var body = Expression.MakeBinary(ExpressionType.Equal, efPropertyCall, Expression.Constant(false));
            var expression = Expression.Lambda(body, parameter);
            entityBuilder.HasQueryFilter(expression);
        }
      }

0

我觉得涉及到 ExpressionReplacingExpressionVisitor 的解决方案过于复杂。我会用反射的方式更加直接地编写代码。

步骤1:将查询过滤器提取到自己的方法中。

private static SetNotSoftDeletedQueryFilter<T>(ModelBuilder builder) where T : class, IDeletableEntity
{
  builder.Entity<T>().HasQueryFilter(m => EF.Property<Boolean>(m, "IsDeleted") == false);
}

步骤2:通过反射获取上述方法的钩子。

private static readonly MethodInfo SetNotSoftDeletedQueryFilterMethod = typeof(ApplicationDbContext)
  .GetMethod(nameof(SetNotSoftDeletedQueryFilter), BindingFlags.Static | BindingFlags.NonPublic)!;

步骤三:调用!

builder.Model.GetEntityTypes()
  .Where(entityType => typeof(IDeletableEntity).IsAssignableFrom(entityType.ClrType))
  .ToList()
  .ForEach(entityType =>
  {
    builder.Entity(entityType.ClrType).Property<Boolean>("IsDeleted");
    //builder.Entity(entityType.ClrType).HasQueryFilter(e => EF.Property<Boolean>(e, "IsDeleted") == false);
    SetNotSoftDeletedQueryFilterMethod.MakeGenericMethod(entityType.ClrType)
      .Invoke(null, new object[] { builder });
  });

0

使用以下代码获取所有实体并过滤属性:

foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            if (entityType.ClrType.GetCustomAttributes(typeof(AuditableAttribute), true).Length > 0)
            {                                       
                modelBuilder.Entity(entityType.Name).Property<bool>("IsRemoved");                   
            }

            var isActiveProperty = entityType.FindProperty("IsRemoved");
            if (isActiveProperty != null && isActiveProperty.ClrType == typeof(bool))
            {
                var entityBuilder = modelBuilder.Entity(entityType.ClrType);
                var parameter = Expression.Parameter(entityType.ClrType, "e");
                var methodInfo = typeof(EF).GetMethod(nameof(EF.Property))!.MakeGenericMethod(typeof(bool))!;
                var efPropertyCall = Expression.Call(null, methodInfo, parameter, Expression.Constant("IsRemoved"));
                var body = Expression.MakeBinary(ExpressionType.Equal, efPropertyCall, Expression.Constant(false));
                var expression = Expression.Lambda(body, parameter);
                entityBuilder.HasQueryFilter(expression);
            }                
        }  

0
我所做的是:
builder.Model.GetEntityTypes()
           .Where(p => typeof(IDeletableEntity).IsAssignableFrom(p.ClrType))
           .ToList()
            .ForEach(entityType =>
            {
                builder.Entity(entityType.ClrType)
                .HasQueryFilter(ConvertFilterExpression<IDeletableEntity>(e => !e.IsDeleted, entityType.ClrType));
            });

并且

 private static LambdaExpression ConvertFilterExpression<TInterface>(
                Expression<Func<TInterface, bool>> filterExpression,
                Type entityType)
    {
        var newParam = Expression.Parameter(entityType);
        var newBody = ReplacingExpressionVisitor.Replace(filterExpression.Parameters.Single(), newParam, filterExpression.Body);

        return Expression.Lambda(newBody, newParam);
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接