SamazoOo
SamazoOo

Reputation: 335

EF Core: Soft delete with shadow properties and query filters

I've created an interface to try to do a soft delete, mixing shadow properties and query filters. But it's not working.

public interface IDeletableEntity {}

And then in my model builder

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

But the line with the query filter doesn't compile. The error I got is "cannot convert lambda expression to type 'lambda expression' because it is not a delegate type"

If I do this it's working.

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

there are any way to do this? It's in order to have an Interface with IDeletableEntity and not have to do it, in every Entity that I want to use a soft delete Entity

Many thanks in advance,

Upvotes: 14

Views: 5122

Answers (8)

MahmoudVahedim
MahmoudVahedim

Reputation: 61

use below code to get all entities and filter a property:

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

Upvotes: 0

Ε Г И І И О
Ε Г И І И О

Reputation: 12341

I feel the solutions involving Expression and ReplacingExpressionVisitor are too convoluted. I would write this in a more straightforward way, using reflection.

Step 1: Extract your query filter to its own method.

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

Step 2: Get a hook to the above method via reflection.

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

Step 3: Invoke!

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

Upvotes: 0

Namig Hajiyev
Namig Hajiyev

Reputation: 1541

For EF core version 6.0 here is an extension function that applies "soft deleting" filter query to all entities that extends to IDeletableEntity interface.

  1. nullable version (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);
        }
      }
  1. non nullable version (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);
        }
      }

Upvotes: 0

SamazoOo
SamazoOo

Reputation: 335

What I did was

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

and

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

Upvotes: 0

Saeed Ganji
Saeed Ganji

Reputation: 307

That does not work for me, .net core 3.1, so I tried the following approach which is kinda similar :

// 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);
        }

Upvotes: 1

Ivan Stoev
Ivan Stoev

Reputation: 205629

HasQueryFilter of the non generic EntityTypeBuilder (as opposed to the generic EntityTypeBuilder<TEntity>) is almost unusable because there is no easy way to create the expected LambdaExpression.

One solution is to build the lambda expression by hand using the Expression class methods:

.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));
});

Another one is to use a prototype expression

Expression<Func<object, bool>> filter = 
    e => EF.Property<bool>(e, "IsDeleted") == false;

and use a parameter replacer to bind the parameter with actual type:

.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));
});

where ReplaceParameter is one of the custom helper extension method I'm using for expression tree manipulation:

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

But most natural solution in my opinion is to move the configuration code in a generic method and call it via reflection. For instance:

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

and then

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

Upvotes: 20

Chamika Goonetilaka
Chamika Goonetilaka

Reputation: 716

A small enhancement to @SamazoOo's answer. You can write an extension method to make it more consistent.

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

Upvotes: 4

SamazoOo
SamazoOo

Reputation: 335

I've found a simple solution for my answer ;-). Thanks anyway Ivan Stoev

The interface is:

public interface IDeletableEntity
{
    bool IsDeleted { get; }
}

And in your model Builder configuration:

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

You need to convertfilterExpression

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

Upvotes: 18

Related Questions