Global query filters in Entity Framework are a powerful feature introduced in EF Core 2.0. They allow you to define filters at the model level that apply automatically to every query involving a particular entity. This eliminates the need to repeatedly include specific filter conditions in your queries. In this article, we'll explore two common use cases for global query filters in ASP.NET Core applications: Multitenancy and Soft Delete.
Why Use Global Query Filters?
Imagine you're working on a multitenant application where every entity is scoped by a TenantId. You'd typically filter queries to include only entities belonging to the current tenant. Without global query filters, you'd need to include the TenantId condition in every query, which can become repetitive and error-prone.
Another scenario is implementing Soft Delete, where instead of physically deleting records, you mark them as deleted using a flag (e.g., IsDeleted). Without a global filter, you’d need to ensure that every query excludes entities where IsDeleted = true.
Setting Up Global Query Filters in EF Core
Tenant and Softdelete Filtering
Let’s say you have an application with the following BaseEntity:
public class BaseEntity
{
public bool IsDeleted { get; set; }
public Guid TenantId { get; set; };
}
Define your DbContext with a global query filter for TenantId:
public class ApplicationContext(DbContextOptions<ApplicationContext> options, IRequestContext context) : DbContext(options)
{
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyGlobalFilter<BaseEntity>(e => !e.IsDeleted && e.TenantId == context.TenantId);
}
}
Tenant specific entity - Product
public class Product : BaseEntity
{
public string Name { get; set; } = null!;
public decimal Price { get; set; }
}
Extension to Register query filters with Convention-Based technique.
public static class ModelBuilderFilterExtensions
{
public static void ApplyGlobalFilter<TEntity>(this ModelBuilder modelBuilder, Expression<Func<TEntity, bool>> expression)
{
var type = typeof(TEntity);
var entities = modelBuilder.Model.GetEntityTypes()
.Where(e => type.IsAssignableFrom(e.ClrType))
.Select(e => e.ClrType);
foreach (var entity in entities)
{
var param = Expression.Parameter(entity);
var body = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), param, expression.Body);
modelBuilder.Entity(entity).HasQueryFilter(Expression.Lambda(body, param));
}
}
}
How It Works
The ApplyGlobalFilter method in ModelBuilderFilterExtensions is responsible for dynamically adding filters to all entities that inherit from a specific type (BaseEntity in this case).
When you inject the ApplicationDbContext into your services or controllers, queries for entities inheriting BaseEntity will automatically include the TenantId filter.
When GetProductsAsync is called in the ProductService, the following occurs:
The ApplicationDbContext applies the global query filters (!IsDeleted && TenantId == context.TenantId) to the Products DbSet.
Only products belonging to the current tenant and not marked as deleted are included in the query results.
This streamlined approach ensures cleaner code and enforces consistent filtering across all queries without additional effort.
public class ProductService(ApplicationDbContext dbContext)
{
public async Task<List<Product>> GetProductsAsync()
{
return await dbContext.Products.ToListAsync();
// Automatically filters by the current TenantId
}
}
Conclusion
EF's global query filter is a fantastic tool for simplifying repetitive filtering logic in applications. Whether you're managing multitenancy with TenantId or implementing soft delete with IsDeleted, they make your code cleaner and more maintainable.
Official doc : https://learn.microsoft.com/en-us/ef/core/querying/filters
Happy coding! 🚀
Top comments (2)
Great article! I’ll apply this approach in my projects.
Great article! It is really helpful