r3plica
r3plica

Reputation: 13387

Using Global Query Filters for all entities

I have recently found global filters, which is great because I have been tasked with implementing soft deletes in my applicaiton. Currently I have done this:

// Query filters https://learn.microsoft.com/en-us/ef/core/querying/filters
modelBuilder.Entity<Address>().HasQueryFilter(m => !m.Deleted);
modelBuilder.Entity<Attribute>().HasQueryFilter(m => !m.Deleted);
modelBuilder.Entity<Brand>().HasQueryFilter(m => !m.Deleted);
modelBuilder.Entity<BrandAddress>().HasQueryFilter(m => !m.Deleted);
modelBuilder.Entity<BrandCategory>().HasQueryFilter(m => !m.Deleted);
modelBuilder.Entity<Category>().HasQueryFilter(m => !m.Deleted);
// many more entity types....

All the entities inherit a BaseModel which looks like this:

public class BaseModel
{
    public Guid CreatedBy { get; set; }
    public Guid UpdatedBy { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime DateUpdated { get; set; }
    public bool Deleted { get; set; }
}

Is it possible to add the query filter for any class that inherits the BaseModel? Something like:

modelBuilder.Entity<BaseModel>().HasQueryFilter(m => !m.Deleted);

So I don't forget (at a later date) to add the query filter for models I add?

Upvotes: 18

Views: 13064

Answers (4)

Guru Stron
Guru Stron

Reputation: 142943

For the latest EF Core version (should work for 3.0 also, for earlier versions expression replacement should be handled manually, see ReplacingExpressionVisitor call) you can automate it using some reflection (minimal amount of it), expression trees and IMutableModel.GetEntityTypes in your OnModelCreating method. Something like this should work:

// define your filter expression tree
Expression<Func<BaseModel, bool>> filterExpr = bm => !bm.Deleted;
foreach (var mutableEntityType in modelBuilder.Model.GetEntityTypes())
{
    // check if current entity type is child of BaseModel
    if (mutableEntityType.ClrType.IsAssignableTo(typeof(BaseModel)))
    {
        // modify expression to handle correct child type
        var parameter = Expression.Parameter(mutableEntityType.ClrType);
        var body = ReplacingExpressionVisitor.Replace(filterExpr.Parameters.First(), parameter, filterExpr.Body);
        var lambdaExpression = Expression.Lambda(body, parameter);

        // set filter
        mutableEntityType.SetQueryFilter(lambdaExpression);
    }
}

Also you can move this to Conventions based approach (via IModelFinalizingConvention).

Upvotes: 33

Svyatoslav Danyliv
Svyatoslav Danyliv

Reputation: 27406

This is generic implementaion of ApplyQueryFilter. It accepts any base entity and applies filter to them. It also preserve previously applied filters, so they can be combined.

No magic strings, just statically typed expressions:

modelBuilder.ApplyQueryFilter<BaseModel>(e => !e.IsDeleted);

// or if soft delete is controlled by interface
modelBuilder.ApplyQueryFilter<ISoftDelete>(e => !e.IsDeleted);

And implemenation:

public static class ModelBuilderExtensions
{
    public static void ApplyQueryFilter<TBaseEntity>(this ModelBuilder builder,
        Expression<Func<TBaseEntity, bool>> filter)
    {
        var acceptableItems = builder.Model.GetEntityTypes()
            .Where(et => typeof(TBaseEntity).IsAssignableFrom(et.ClrType))
            .ToList();

        foreach (var entityType in acceptableItems)
        {
            var entityParam = Expression.Parameter(entityType.ClrType, "e");

            // replacing parameter with actual type
            var filterBody = ReplacingExpressionVisitor.Replace(filter.Parameters[0], entityParam, filter.Body);

            var filterLambda = entityType.GetQueryFilter();
            // Other filter already present, combine them
            if (filterLambda != null)
            {
                filterBody = ReplacingExpressionVisitor.Replace(entityParam, filterLambda.Parameters[0], filterBody);
                filterBody = Expression.AndAlso(filterLambda.Body, filterBody);
                filterLambda = Expression.Lambda(filterBody, filterLambda.Parameters);
            }
            else
            {
                filterLambda = Expression.Lambda(filterBody, entityParam);
            }               

            entityType.SetQueryFilter(filterLambda);
        }
    }
}

Upvotes: 3

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

abdusco
abdusco

Reputation: 11101

You need to construct a lambda expression at runtime for:

instance => !instance.IsDeleted

Now, assume we have this interface, and a number of entities that implement this interface (directly or transitively):

interface ISoftDelete
{
    bool IsDeleted { get; set; }
}

class Product : ISoftDelete
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }
    // ...
}

We want to apply soft-delete query filter to all entities that implement this interface.

To find all entities registered in a DbContext model, we can use IMutableModel.GetEntityTypes(). Then we filter all entities implementing ISoftDelete and add set a custom query filter.

Here's an extension method that you can use directly:

internal static class SoftDeleteModelBuilderExtensions
{
    public static ModelBuilder ApplySoftDeleteQueryFilter(this ModelBuilder modelBuilder)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            if (!typeof(ISoftDelete).IsAssignableFrom(entityType.ClrType))
            {
                continue;
            }

            var param = Expression.Parameter(entityType.ClrType, "entity");
            var prop = Expression.PropertyOrField(param, nameof(ISoftDelete.IsDeleted));
            var entityNotDeleted = Expression.Lambda(Expression.Equal(prop, Expression.Constant(false)), param);

            entityType.SetQueryFilter(entityNotDeleted);
        }

        return modelBuilder;
    }
}

Now, we can use this in our DbContext:

class AppDbContext : DbContext
{
    // ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>();
        modelBuilder.ApplySoftDeleteQueryFilter(); // <-- must come after all entity definitions
    }
}

Upvotes: 8

Related Questions