Named Query Filters in EF Core 10: Multiple Query Filters per Entity
Learn named query filters in EF Core 10. Add multiple global filters per entity, turn off just one by name, and keep soft-delete plus multi-tenant code clean.
Think about a school notice board. The board shows notices for everyone. But some notices are only for Class 6, and some are only for the cricket team. If the board could show just one rule at a time, you would be stuck. You could not say "show Class 6 notices" and "hide old expired notices" together.
That was the old problem with global query filters in Entity Framework Core. Each table could have only one filter. If you wanted two rules at the same time, you had to squeeze them into one line with &&. And if you wanted to switch off just one rule, you could not. You had to switch off all of them.
EF Core 10 fixes this with named query filters. Now you can put many filters on one entity, give each one a name, and turn a single one off whenever you need. In this guide you will learn what they are, why they matter, and how to use them with small, friendly examples.
A quick refresher: what is a global query filter?
A global query filter is a rule that EF Core adds to every query for an entity, automatically. You write it once in your model. After that, EF Core quietly adds it to the WHERE part of the SQL each time you read that table.
Two very common uses are:
- Soft delete — you never really delete a row. You just set
IsDeleted = true. A filter then hides those rows so the rest of your app never sees them. - Multi-tenancy — many customers (tenants) share one database. A filter makes sure each tenant only sees their own rows.
Here is the classic, single-filter way. It still works in EF Core 10.
public class AppDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// The old way: one unnamed filter per entity.
modelBuilder.Entity<Order>()
.HasQueryFilter(order => !order.IsDeleted);
}
}From now on, context.Orders.ToListAsync() only returns rows where IsDeleted is false. You did not write that condition in your query. EF Core added it for you.
The old pain: only one filter per entity
The trouble started when you needed two rules at once. Say you want both soft delete and multi-tenancy on Order.
Before EF Core 10, calling HasQueryFilter a second time did not add a second filter. It replaced the first one. So your soft-delete rule would silently vanish. The only fix was to mash both rules into a single expression:
// The old workaround: cram every rule into one filter.
modelBuilder.Entity<Order>()
.HasQueryFilter(order =>
!order.IsDeleted &&
order.TenantId == _tenantProvider.CurrentTenantId);This worked, but it had two real problems.
- It got messy fast. Add a third or fourth rule and that one line becomes hard to read and hard to test.
- You could not turn off just one rule. The only tool was
IgnoreQueryFilters(), which switched off everything. So if an admin page needed to see deleted orders, callingIgnoreQueryFilters()also removed the tenant rule. That tenant rule keeps customers from seeing each other's data. Turning it off by accident is a real security risk.
The table below shows the gap clearly.
| What you want to do | Before EF Core 10 | With named filters in EF Core 10 |
|---|---|---|
| Add two filters to one entity | Merge them with && into one filter | Call HasQueryFilter twice with names |
| See deleted rows but keep tenant safety | Not possible — IgnoreQueryFilters() removed both | Skip only "SoftDeleteFilter" by name |
| Read which rules exist | Untangle one long expression | Each filter has a clear name |
| Risk of leaking other tenants' data | Higher | Lower |
The new way: named query filters
EF Core 10 lets you call HasQueryFilter many times for one entity. You just give each call a name (a simple string). Each filter is kept separately. They all still run together, joined with AND.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasQueryFilter("SoftDeleteFilter", order => !order.IsDeleted)
.HasQueryFilter("TenantFilter", order => order.TenantId == _tenantId);
}That is it. Now every query on Order gets both rules. The generated SQL looks like WHERE IsDeleted = 0 AND TenantId = @tenant. Nothing changed about how the database runs. The only new thing is that each rule has a name you can hold on to.
Turning off just one filter by name
Here is the part that makes named filters worth it. You can switch off one filter and keep the rest. You pass a list of names to IgnoreQueryFilters.
// Admin view: show deleted orders too, but STILL stay inside this tenant.
var ordersIncludingDeleted = await context.Orders
.IgnoreQueryFilters(["SoftDeleteFilter"])
.ToListAsync();In this query, the SoftDeleteFilter is skipped, so you see deleted rows. But the TenantFilter is still active, so you never see another customer's orders. Before EF Core 10 you simply could not do this in a safe way.
You can name more than one filter to skip several at once:
// Skip two named filters in one call.
var everything = await context.Orders
.IgnoreQueryFilters(["SoftDeleteFilter", "TenantFilter"])
.ToListAsync();And the old no-argument call still works to remove all filters:
// Remove every filter on this entity (use with care).
var raw = await context.Orders
.IgnoreQueryFilters()
.ToListAsync();Choosing which filters to skip
Steps
Need all rows?
Call IgnoreQueryFilters() with no name
Skip one rule?
Pass that filter's name in a list
Keep safety rule?
Leave the tenant filter out of the skip list
One important rule: named or unnamed, not both
For a single entity you must pick one style:
- Either one unnamed filter (the old way), or
- One or more named filters (the new way).
You cannot mix them on the same entity. If you add both a named and an unnamed filter to Order, EF Core throws an error when it builds the model. The error shows up at startup, not at runtime, so you find it early.
Different entities can use different styles, though. Order can use named filters while Product uses a single unnamed filter. The rule is only about mixing styles within one entity.
| Setup on one entity | Allowed? |
|---|---|
| One unnamed filter | Yes |
| Two named filters | Yes |
| One unnamed + one named | No (error at model build) |
| Zero filters | Yes |
Avoiding magic strings
Filter names are plain strings. Plain strings are easy to misspell. If you write "SoftDeleteFilter" in the model but "SoftDeletFilter" in a query, EF Core will not find a match, and your skip will quietly do nothing. That is a sneaky bug.
The fix is simple: keep your names in one place as constants, and use them everywhere.
public static class OrderFilters
{
public const string SoftDelete = "SoftDeleteFilter";
public const string Tenant = "TenantFilter";
}
// In the model:
modelBuilder.Entity<Order>()
.HasQueryFilter(OrderFilters.SoftDelete, o => !o.IsDeleted)
.HasQueryFilter(OrderFilters.Tenant, o => o.TenantId == _tenantId);
// In a query:
var data = await context.Orders
.IgnoreQueryFilters([OrderFilters.SoftDelete])
.ToListAsync();Now the compiler is on your side. A typo in OrderFilters.SoftDelete will not even build. This small habit saves real debugging time later.
A fuller example: soft delete plus multi-tenancy
Let us put the pieces together in a small but realistic context. The tenant id usually comes from the logged-in user, so we pass it in through the constructor.
public class ShopDbContext : DbContext
{
private readonly Guid _tenantId;
public ShopDbContext(DbContextOptions<ShopDbContext> options, ITenantProvider tenant)
: base(options)
{
_tenantId = tenant.CurrentTenantId;
}
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasQueryFilter(OrderFilters.SoftDelete, o => !o.IsDeleted)
.HasQueryFilter(OrderFilters.Tenant, o => o.TenantId == _tenantId);
}
}With this in place:
- A normal list page calls
context.Orders.ToListAsync()and is safe by default. It hides deleted rows and stays inside the tenant. - An admin "recycle bin" page calls
IgnoreQueryFilters([OrderFilters.SoftDelete])to show deleted rows, while the tenant rule keeps things locked down. - A background data-cleanup job that must cross tenants can call
IgnoreQueryFilters([OrderFilters.Tenant])on purpose, with a clear name showing exactly what it is doing.
The big win here is readability and safety. When you read IgnoreQueryFilters([OrderFilters.SoftDelete]), you know in one glance exactly which rule is off and which stays on. The old IgnoreQueryFilters() told you nothing — it just removed everything and hoped for the best.
From request to safe data
Steps
User request
Page asks for orders
Build query
LINQ on context.Orders
Apply named filters
Active filters joined with AND
Run SQL
WHERE clause sent to database
Return rows
Only allowed rows come back
Things to keep in mind
A few small notes will save you trouble.
- Filters use
AND, neverOR. All active named filters are combined withAND. If you truly need anORbetween two conditions, put thatORinside a single filter expression. Do not expect two separate named filters to act asOR. - Filters run on the database, so keep them simple. Whatever you write must translate to SQL. Calling a custom C# method that cannot become SQL will fail. Stick to simple comparisons on entity properties.
- Naming does not slow things down. A named filter and an unnamed filter generate the same SQL. The name lives only in your model. There is no extra query and no extra cost at runtime.
- It is an EF Core 10 feature. You need the EF Core 10 packages on .NET 10. Since .NET 10 is an LTS release, this is a safe base for new projects.
When should you use named filters?
Reach for named filters when an entity needs more than one automatic rule, or when you sometimes need to turn one rule off while keeping the others. The two most common cases are soft delete plus multi-tenancy, exactly as shown above.
If an entity only ever needs a single rule and you never turn it off selectively, the plain unnamed filter is still perfectly fine. You do not have to rewrite old code. Named filters are a new tool, not a forced change.
Quick recap
- Before EF Core 10, each entity could have only one global query filter, and
IgnoreQueryFilters()turned off all of them at once. - Named query filters in EF Core 10 let you add many filters to one entity by calling
HasQueryFilterwith a name each time. - All active filters are joined with AND and added to the SQL
WHEREclause, just like before. Naming adds no runtime cost. - You can skip one filter by name with
IgnoreQueryFilters(["FilterName"]), while the other filters keep running. This is great for showing deleted rows while staying inside a tenant. - For one entity, use either an unnamed filter or named filters, never both. Mixing them throws an error at model-build time.
- Store filter names as constants so a typo cannot slip through and silently break a skip.
- This ships with EF Core 10 on .NET 10 (LTS), so it is safe for new projects.
References and further reading
- Global Query Filters - EF Core (Microsoft Learn)
- What's New in EF Core 10 (Microsoft Learn)
- Named Query Filters in EF 10 — Milan Jovanovic
- Named global query filters in EF Core 10 — Tim Deschryver
- Global Query Filters in EF Core — codewithmukesh
- Named query filters (original feature request) — dotnet/efcore #8576
Related Posts
Strongly Typed IDs in .NET: A Safer Way to Identify Entities
Learn how strongly typed IDs in .NET stop you from mixing up entity identifiers, catch bugs at compile time, and work cleanly with EF Core.
How I Optimized an API Endpoint to Make It 15x Faster
A simple, step-by-step story of how I made a slow ASP.NET Core API endpoint 15x faster using EF Core projection, AsNoTracking, paging, and indexes.
Named Global Query Filters in EF Core 10: Multiple Filters Per Entity
Learn how EF Core 10 adds named global query filters so you can use multiple filters per entity and turn off just one filter when you need to.
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.
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.
What's New in EF Core 10: LeftJoin and RightJoin in LINQ
Learn the new LeftJoin and RightJoin LINQ operators in EF Core 10. Simple examples, SQL mapping, and clear tables to help you write cleaner join queries.