Entity Framework Core Global Query Filters: Built-In Where Clauses for Every Query
EF Core global query filters automatically apply a WHERE clause to every query for an entity, enabling soft deletes and tenant isolation without repeating filters. This post covers configuration, a worked example, and the trade-offs you need to test.
21 Dec 2025, 05:26 UTC

.Where(b => !b.IsDeleted) clause in a repository method. The result is data that disappears for some users or, worse, rows that should be hidden still appearing in reports. Entity Framework Core global query filters solve this by letting you declare the exclusion once, in OnModelCreating, and have the database provider inject the predicate into every query that touches the entity.
How global query filters work
A global query filter is a lambda expression attached to an entity type via the HasQueryFilter method. The EF Core query translator emits the predicate as part of the SQL WHERE clause, so the filter runs entirely in the database, not in memory. This means the filter is free of runtime overhead beyond the generated query, and it applies regardless of whether you use LINQ methods, raw SQL, or generated projections. Unfamiliar term: OnModelCreating is the conventional method in a DbContext subclass where you configure the model; the filter persists for the lifetime of the context.
Configuring a global query filter + worked example
public class BloggingContext : DbContext
{
public DbSet Blogs { get; set; }
public DbSet Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Soft‑delete: never return blogs where IsDeleted is true
modelBuilder.Entity().HasQueryFilter(b => !b.IsDeleted);
}
}
Place this method inside any DbContext subclass. The filter predicate runs against the mapped table; if the entity has an IsDeleted boolean column, the generated SQL will include WHERE [IsDeleted] = 0 (or the database-specific equivalent). This is the concrete example: one line of configuration gives every query an automatic soft-delete guard.
Trade-offs, limitations, and when to bypass the filter
- Non-indexed filter columns: if the filtered column has no index, every query may perform a full table scan, which hurts performance on large tables. Adding a database index on the column mitigates this risk.
- Included navigations are also filtered: loading
context.Posts.Include(p => p.Blog)still applies theIsDeletedfilter to associated blogs, potentially yielding empty navigation collections. AsNoTrackingstill respects the filter: theWHEREclause remains; only change-tracking behavior differs.- Per-query bypass: call
.IgnoreQueryFilters()on the specific query where you need unfiltered data. This does not remove the model-level filter; future queries remain filtered.
If you’re adding soft-delete support to an existing EF Core model, start by adding the filter in OnModelCreating, verify the behavior with the steps below, and add a database index on the filter column before promoting to production.
To verify the behavior in your own project, run the following checks against a BloggingContext instance (no special permissions required; simply build and run the application). Replace Blog and IsDeleted with your entity and column names.
- Run a standard query:
context.Blogs.ToList()— should return only non-deleted blogs. - Run an unfiltered query:
context.Blogs.IgnoreQueryFilters().ToList()— should include deleted blogs. - Load a navigation with include:
context.Posts.Include(p => p.Blog).ToList()— check that deleted blogs do not appear in theBlognavigation. - Bypass the navigation filter:
context.Posts.IgnoreQueryFilters().Include(p => p.Blog).ToList()— should include blogs regardless ofIsDeleted.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.