EF Core: Soft delete with shadow properties and query filters

后端 未结 5 1020
旧时难觅i
旧时难觅i 2020-12-03 05:47

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 {         


        
相关标签:
5条回答
  • 2020-12-03 06:13

    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);
                    }
    
    0 讨论(0)
  • 2020-12-03 06:14

    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);
        }
    
    0 讨论(0)
  • 2020-12-03 06:19

    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);
            }
    
    
    0 讨论(0)
  • 2020-12-03 06:29

    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 })
    );
    
    0 讨论(0)
  • 2020-12-03 06:33

    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);
        }
    
    0 讨论(0)
提交回复
热议问题