Global query filters are a feature provided by Entity Framework. These filters automatically apply filtering conditions to all database queries involving a specific entity.
The primary purpose of global query filters is to enable data access restrictions or soft delete functionality at the database level without the need to explicitly add filtering conditions to every single query. This can be particularly useful in scenarios where you want to:
- Soft delete
Implement soft deletes: Soft deletes involve marking records as deleted rather than physically removing them from the database. You can use global query filters to automatically exclude these “soft deleted” records from all queries, effectively hiding them from application users. - Enfore data access policies
Enforce data access policies: You can use global query filters to restrict access to certain data based on user roles or other criteria. For example, you could ensure that users can only access records that belong to their own account or organization. - Implement multi-tenancy
Implement multi-tenancy: In multi-tenant applications, where multiple customers or organizations share the same database, you can use global query filters to ensure that each tenant can only access their own data.
By implementing global query filters, code maintainability is enhanced, and the risk of inadvertently retrieving or modifying data that should be excluded or protected is reduced. They provide a centralized and consistent way to implement data access restrictions and filtering conditions, making it easier to manage data access policies in your application.
This article explains how to implement soft delete.
There are multiple ways to implement a global query filters.
1. Specify the filter in your OnModelCreating (or in your entity configuration)
protected override void OnModelCreating(ModelBuilder builder)
{
builder
.Entity<Product>()
.HasQueryFilter(x => x.IsDeleted == false);
}
2. Use an interface
By using an interface you can automatically add the query filter to every entity inheriting this interface.
Step 1 – Create the interface
Add an interface called ‘ISoftDelete’. The interface should contain the property or properties your entity will need to implement to be able to set the filter.
public interface ISoftDelete
{
public bool IsDeleted { get; set; }
}
Step 2 – Modify your DbContext
public class ExampleDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
foreach (var entityType in builder.Model.GetEntityTypes())
{
ConfigureGlobalFiltersMethodInfo
.MakeGenericMethod(entityType.ClrType)
.Invoke(this, new object[] { builder, entityType });
}
}
protected void ConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder, IMutableEntityType entityType)
where TEntity : class
{
if (entityType.BaseType != null || !ShouldFilterEntity<TEntity>(entityType)) return;
var filterExpression = CreateFilterExpression<TEntity>();
if (filterExpression != null)
{
modelBuilder.Entity<TEntity>().HasQueryFilter(filterExpression);
}
}
protected virtual bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType) where TEntity : class
{
return typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity));
}
protected virtual Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>()
where TEntity : class
{
Expression<Func<TEntity, bool>> expression = null;
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
{
Expression<Func<TEntity, bool>> softDeleteFilter = e => !((ISoftDelete)e).IsDeleted;
expression = softDeleteFilter;
}
return expression!;
}
}
Let’s examine this code:
In the OnModelCreating
method, the code iterates through all the entity types in the model by calling builder.Model.GetEntityTypes()
. For each entity type, it invokes the ConfigureGlobalFiltersMethodInfo
method, passing the current entity type as a generic argument. This method configures the global query filter for the entity type if it meets the criteria.
The ConfigureGlobalFilters
method takes a generic type parameter TEntity
, representing the current entity type. It checks whether the entity has a base type (i.e., it’s not the root entity) and whether it should be filtered based on the ShouldFilterEntity
method.
The ShouldFilterEntity
method is a virtual method that returns a boolean value indicating whether the given entity should be filtered. In this case, it checks if the entity implements the ISoftDelete
interface, signifying that it supports soft deletes.
If the entity should be filtered, the code calls the CreateFilterExpression
method to generate the filter expression for that entity type.
The CreateFilterExpression
method also uses reflection to check if the entity implements the ISoftDelete
interface. If it does, it creates an expression that represents the condition for the soft delete filter (i.e., e => !((ISoftDelete)e).IsDeleted
).
Finally, if the filter expression is not null (indicating that the entity should be filtered and has a valid filter expression), the code applies the global query filter to the entity using modelBuilder.Entity<TEntity>().HasQueryFilter(filterExpression)
.
Summary
To summarize, this code dynamically applies a soft delete global query filter to all entities that implement the ISoftDelete
interface. The filter ensures that any queries involving these entities will automatically exclude soft-deleted records. This custom implementation centralizes the filtering logic and consistently applies it across multiple entities without manually adding filter expressions to each entity configuration.
Learn how to configure an interceptor in entity framework core
https://docubear.com/entity-framework-core-interceptors