How to Use Global Query Filters in EF Core (Beginner Guide)
Learn EF Core global query filters with simple examples for soft delete and multi-tenancy, plus the new named filters in EF Core 10.
How to Use Global Query Filters in EF Core
Imagine you work at a busy sweet shop. Every morning your manager gives you one standing rule: "Never put expired sweets on the shelf." You do not check this rule again for every single box. It just becomes part of how you work. Whatever box you pick up, you quietly skip the expired ones.
A global query filter in EF Core is exactly this kind of standing rule. You write it once. After that, EF Core quietly applies it to every query for that table, without you typing the condition again and again.
In this guide you will learn what these filters are, why they save you from painful bugs, how to write them, and how the new named filters in EF Core 10 make them even better. We will keep the language simple and the examples small.
What is a global query filter?
A global query filter is a small piece of C# logic that you attach to an entity type. EF Core then adds it as a WHERE clause to every SQL query it builds for that entity.
You configure it once inside OnModelCreating in your DbContext. After that, every ToList, every FirstOrDefault, every Include, and every navigation load for that entity carries the filter automatically.
The big idea is safety by default. Instead of trusting every developer to remember a condition, the filter is baked into the model. You cannot forget it, because you never type it.
Two everyday problems it solves
Most teams reach for global query filters to solve two common problems. Let us look at both in plain words.
| Problem | Without filters | With a global filter |
|---|---|---|
| Soft delete | Every query must add WHERE IsDeleted = 0 by hand | The filter hides deleted rows for you |
| Multi-tenancy | Every query must add WHERE TenantId = @id by hand | The filter limits rows to the current tenant |
Soft delete
"Soft delete" means you do not really erase a row. You just mark it as deleted by setting a flag, like IsDeleted = true. The data stays in the table, so you can recover it later or keep an audit trail. But normal screens should never show those rows.
Multi-tenancy
A "multi-tenant" app serves many customers (tenants) from one database. Customer A must never see Customer B's data. A filter on TenantId keeps each customer locked inside their own rows.
Why filters beat manual conditions
Steps
One rule
Write filter once in DbContext
Every query
EF Core applies it automatically
Fewer bugs
Nobody forgets the WHERE clause
Safer data
Deleted or other-tenant rows stay hidden
Your first filter: soft delete
Let us build a tiny example. We have a Product entity with an IsDeleted flag. We want every query to skip deleted products.
First, the entity:
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
public bool IsDeleted { get; set; }
}Now we add the filter inside OnModelCreating. The method is called HasQueryFilter, and we give it a small lambda that returns true for the rows we want to keep:
public class ShopDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.HasQueryFilter(p => !p.IsDeleted);
}
}That single line changes everything. Now this normal query:
var products = await context.Products.ToListAsync();...produces SQL that already includes WHERE [p].[IsDeleted] = 0. You did not write that condition. EF Core added it for you. Deleted products simply do not appear.
The lambda reads as "keep the product when it is not deleted". The ! means "not". So !p.IsDeleted is true only when IsDeleted is false.
Filters follow your relationships too
Here is a detail that surprises many beginners. Filters do not only apply when you query an entity directly. They also apply when EF Core loads that entity through a navigation property.
Say an Order has many Product items. When you write Include(o => o.Products), the soft-delete filter on Product still runs. Deleted products are skipped inside the included list as well. This keeps your data consistent everywhere, not just on the main query.
That is powerful, but it means you should keep filters simple and predictable. A heavy or confusing filter spreads its cost to every related load too.
Turning a filter off on purpose
Sometimes you genuinely need the hidden rows. An admin "recycle bin" screen, a report, or a cleanup job may need to see deleted products. For these cases EF Core gives you IgnoreQueryFilters.
var allProducts = await context.Products
.IgnoreQueryFilters()
.ToListAsync();This query ignores all filters on that entity and returns every row, deleted or not. Use it carefully and only where you truly mean it. In a multi-tenant app especially, switching filters off can leak data, so treat IgnoreQueryFilters as a sharp tool.
When to ignore filters
Steps
Normal screen
Filters ON, hide deleted rows
Admin tool
Filters OFF to show recycle bin
Cleanup job
Filters OFF to purge old rows
Report
Decide per query, be careful
Adding multi-tenancy to the mix
Now imagine our shop app serves many stores. Each Product belongs to a store through a TenantId. We want each store to see only its own products.
The classic trick is to read the current tenant id from a service, then use it inside the filter. EF Core captures the value when it builds the model query.
public class ShopDbContext : DbContext
{
private readonly ITenantProvider _tenant;
public ShopDbContext(DbContextOptions options, ITenantProvider tenant)
: base(options)
{
_tenant = tenant;
}
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.HasQueryFilter(p =>
!p.IsDeleted &&
p.TenantId == _tenant.CurrentTenantId);
}
}Notice how we joined two conditions with && ("and"). Before EF Core 10 this was the only way to have more than one rule on the same entity: stuff them all into one big lambda.
That worked, but it had a real weakness. Because both rules lived in one filter, IgnoreQueryFilters was all-or-nothing. If an admin screen wanted to see deleted rows, it had to switch off the tenant rule too, which is dangerous. You could not say "ignore only the soft-delete part".
EF Core 10: named query filters
EF Core 10 fixes exactly this problem. You can now give each filter a name and attach several filters to the same entity. Each one stands on its own.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.HasQueryFilter("SoftDelete", p => !p.IsDeleted)
.HasQueryFilter("Tenant", p => p.TenantId == _tenant.CurrentTenantId);
}Now the two rules are separate. EF Core still combines them with "and" when it runs queries, so by default you get both. But you can manage them one at a time.
To turn off just one filter, pass its name to IgnoreQueryFilters:
// Show deleted products, but STILL stay inside the current tenant.
var deletedForThisStore = await context.Products
.IgnoreQueryFilters(new[] { "SoftDelete" })
.ToListAsync();This is the part that makes named filters so valuable. The tenant rule stays on, so you cannot accidentally see another store's data, while the soft-delete rule is lifted for this one query. That is much safer than the old all-or-nothing approach.
Here is a quick comparison of the old style and the new style.
| Feature | Before EF Core 10 | EF Core 10 named filters |
|---|---|---|
| Filters per entity | One filter, combined with && | Many filters, each named |
| Disable one rule | Not possible | IgnoreQueryFilters(new[] { "Name" }) |
| Disable everything | IgnoreQueryFilters() | IgnoreQueryFilters() still works |
| Readability | One long lambda | Short, separate rules |
A small but important rule about parameters
When a filter uses a value like _tenant.CurrentTenantId, EF Core reads that value at the moment it first builds the query plan and turns it into a SQL parameter. Each time the tenant changes between requests, EF Core uses the new value, because a fresh DbContext is created per request in most web apps.
This is why you should create a new DbContext for each web request (the default in ASP.NET Core dependency injection). If you reuse one long-lived context across tenants, the filter could capture a stale value. Short-lived contexts keep filters correct and predictable.
Save changes can still skip the flag
A filter only changes how you read data. It does not change how you write it. So for soft delete you still need to actually set IsDeleted = true instead of removing the row.
A common pattern is to override SaveChanges and turn deletes into flag updates:
public override int SaveChanges()
{
foreach (var entry in ChangeTracker.Entries<Product>())
{
if (entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
}
}
return base.SaveChanges();
}Now when your code calls Remove(product) and then SaveChanges, the row is not truly deleted. It is just flagged. And because of your read filter, it disappears from normal queries. The two pieces work together: the override hides the row on write-time, the filter hides it on read-time.
How the whole flow fits together
Let us put the moving parts in order so the picture is clear.
End-to-end soft delete with filters
Steps
Remove + Save
SaveChanges flips IsDeleted to true
Flag set
Row stays in the table
Normal read
Filter hides flagged rows
Admin read
IgnoreQueryFilters shows them
Common mistakes to avoid
Filters are friendly, but a few traps catch beginners. Keep these in mind.
- Forgetting filters apply to includes. If a related list looks shorter than expected, a filter on the child entity may be doing its job. That is usually correct, not a bug.
- Heavy logic in a filter. The filter runs on every query, so keep it cheap. Avoid calling methods EF Core cannot translate to SQL.
- Relying on
IgnoreQueryFilterstoo often. If half your code turns filters off, the filter is no longer protecting you. Reach for it only in clearly named, special cases. - Mixing required navigations with filters. A filter that hides a parent row can make a required relationship look broken. Test these cases.
- Assuming filters change writes. They do not. You still set flags or tenant ids yourself when saving.
When not to use a global filter
Filters are great for rules that apply to almost every query. They are a poor fit for one-off conditions. If only one screen needs a condition, just add a normal Where clause there. Save global filters for the rules that must hold true everywhere, like soft delete and tenant isolation. Using them for narrow cases makes your queries confusing, because the WHERE clause is hidden from the reader.
Quick recap
- A global query filter is a rule you write once that EF Core adds to every query for an entity.
- Configure it in
OnModelCreatingwithHasQueryFilter. - The two classic uses are soft delete (
!p.IsDeleted) and multi-tenancy (p.TenantId == currentTenant). - Filters also apply to
Includeand navigation loads, keeping data consistent. - Use
IgnoreQueryFilters()to switch filters off for special queries like admin tools. - EF Core 10 adds named filters, so you can have many filters per entity and disable just one with
IgnoreQueryFilters(new[] { "Name" }). - Filters only change reads. For soft delete you still set the flag yourself, often by overriding
SaveChanges. - Use a fresh
DbContextper request so parameter values like the tenant id stay correct.
References and further reading
- Global Query Filters - EF Core (Microsoft Learn)
- Named Query Filters in EF 10 (Milan Jovanovic)
- Named global query filters in EF Core 10 (Tim Deschryver)
- Global Query Filters - Soft Delete & Multi-Tenancy (codewithmukesh)
- Named Global Query Filters Were Updated in EF Core 10 (antondevtips)
Related Posts
Soft Delete with EF Core: Delete Data Without Losing It
Learn soft delete in EF Core the right way. Use an interceptor and global query filters to hide deleted rows automatically, with simple examples, diagrams, code, and best practices for .NET 10.
5 EF Core Features You Need to Know (Beginner Friendly)
Learn 5 must-know EF Core features with simple examples: change tracking, AsNoTracking, eager loading, bulk ExecuteUpdate, and query filters.
Global Query Filters in EF Core: Soft Delete and Multi-Tenancy Made Easy
Learn EF Core global query filters with simple examples for soft delete and multi-tenancy, plus the new named filters in .NET 10.
How to Use EF Core Interceptors: A Beginner-Friendly Guide
Learn EF Core interceptors step by step. Add auditing, soft delete, logging, and timing to your DbContext with clean, reusable code and zero clutter.
Understanding Change Tracking for Better Performance in EF Core
Learn how EF Core change tracking works, the entity states it uses, and simple tricks like AsNoTracking to make your .NET apps faster.
Calling Views, Stored Procedures and Functions in EF Core
A friendly, beginner guide to calling database views, stored procedures, and functions in EF Core with FromSql, SqlQuery, ExecuteSql, and ToView.