When working with databases, it’s common to need consistent filtering across multiple queries.
Without a built-in solution, we often have to manually apply filters in every query, increasing the risk of inconsistencies and missing filters.
This repetitive logic not only adds complexity but also makes the code harder to maintain.
To address this, Entity Framework Core introduces Query Filters.
EF Core Query Filters
Query Filters allows us to define global filtering rules at the entity level, ensuring they are consistently applied across all queries.
They are basically LINQ query predicates that are automatically applied to entity types at the metadata level.
Once defined, EF Core ensures that these filters are automatically applied to all queries involving the specified entity types. This includes not only direct queries but also entities retrieved through navigation properties or the include method.
Common Use Cases for Query Filters:
- Soft Delete - Automatically exclude entities where IsDeleted = true
- Multi-tenancy - Restrict data access based on a TenantId, ensuring users only access their own data.
In this blog post, we'll take a closer look at the Product entity.
public sealed class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public bool IsDeleted { get; set; }
public Product(string name, string description, decimal price)
{
Id = Guid.NewGuid();
Name = name;
Description = description;
Price = price;
}
public void Update(string description, decimal price)
{
Description = description;
Price = price;
}
public void Delete() => IsDeleted = true;
}
The Product entity supports soft deletion, meaning it won’t be physically removed from the database. Instead, its IsDeleted property will be set to true when a delete action occurs.
This is useful when you need to retain data for auditing, recovery or historical reference while preventing it from appearing in regular queries.
Without a query filter, we would have to manually add a condition to every query to exclude products where the IsDeleted property is set to true:
var product = await dbContext.Products
.FirstOrDefaultAsync(product =>
!product.IsDeleted &&
product.Id == id);
This brings us to a common problem: the risk of forgetting to include shared conditions in queries.
Query filters easily solve this problem. They can be set in the OnModelCreating method or in the EntityTypeConfiguration by calling the HasQueryFilter method and passing the common condition:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.HasQueryFilter(product => !product.IsDeleted);
}
With the query filter in place, our queries no longer need to include the common condition:
var product = await dbContext.Products
.FirstOrDefaultAsync(product => product.Id == id);
NOTE: You can only set one query filter per entity. If you have multiple conditions, you'll need to chain them together using && in the expression.
Ignore Query Filters
Just because we have a common check doesn’t mean it must always be applied.
When we want to disable the query filter, all we need to do is use IgnoreQueryFilters for that specific query and the filter will not be applied.
var products = await dbContext.Products
.IgnoreQueryFilters()
.ToListAsync();
Conclusion
Consistent filtering across multiple queries is essential when working with databases.
Entity Framework Core's Query Filters provide a simple and effective solution to this problem by allowing global filtering rules to be applied at the entity level.
By using query filters, you can automatically exclude soft-deleted records, enforce multi-tenancy and apply other common conditions.
EF Core also offers the flexibility to disable query filters for specific queries when needed, using IgnoreQueryFilters.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!