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