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.
Imagine a library in your school. There is a rule that hidden, damaged books should never be shown to students. There is another rule that students from Class 6 should only see books meant for Class 6. These are two separate rules, but both run quietly in the background every time someone asks for a list of books.
Now imagine the librarian (a teacher, or a senior librarian) walks in. The librarian needs to see the damaged books to fix them, but still wants the Class 6 rule to stay on. With the old system, the librarian had only one big switch: either follow all the rules, or follow none of them. There was no way to switch off just the "hide damaged books" rule while keeping the "Class 6 only" rule on.
EF Core 10 fixes exactly this problem for your database code. Each rule can now have a name. And you can switch off one rule by its name while the others keep working. That is what named global query filters are about.
A quick refresher: what is a global query filter?
A global query filter is a rule you attach to an entity type once. After that, EF Core adds it to every query for that entity automatically. You do not have to remember to write the rule in each query.
Two very common uses are:
- Soft delete. Instead of really deleting a row, you mark it with
IsDeleted = true. A filter then hides those rows from normal queries. - Multi-tenancy. Many customers share one database. A filter makes sure each customer only sees their own rows.
Here is what a single, classic filter looked like before EF Core 10.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.HasQueryFilter(b => !b.IsDeleted);
}Every query on Book now quietly adds WHERE IsDeleted = 0. Simple and helpful.
The old pain: only one filter per entity
Before EF Core 10 you could call HasQueryFilter only once per entity. If you called it twice, the second call replaced the first one. So people combined many rules into one big expression using the && (and) operator.
modelBuilder.Entity<Book>()
.HasQueryFilter(b => !b.IsDeleted && b.TenantId == _currentTenantId);This works, but it has a hidden problem. The two rules are now glued together. When you wanted to ignore filters in a query, you had only one tool: IgnoreQueryFilters(). And it was all-or-nothing. It switched off the whole glued expression. You could not say "keep the tenant rule but drop the soft-delete rule." You lost both at once.
That last part, seeing other tenants' rows, is scary. In a multi-tenant app you almost never want to drop the tenant rule, but you might often want to see soft-deleted rows (for an admin screen, a cleanup job, or a report). The old design forced you to choose between safety and convenience.
The EF Core 10 change: give each filter a name
EF Core 10 lets you call HasQueryFilter more than once on the same entity, as long as you give each call a name (a string key). Each named filter is stored on its own. You can later switch any one of them off by name.
public static class BookFilters
{
public const string SoftDelete = "SoftDeleteFilter";
public const string Tenant = "TenantFilter";
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.HasQueryFilter(BookFilters.SoftDelete, b => !b.IsDeleted)
.HasQueryFilter(BookFilters.Tenant, b => b.TenantId == _currentTenantId);
}Notice the small but important habit above: the names live in const string fields, not as loose text scattered around the code. A typo in a magic string like "SoftDleteFilter" would not fail at compile time. It would just silently do nothing useful at runtime. Constants protect you from that.
Both filters still run on every normal query. Nothing changes for the everyday case. You get WHERE IsDeleted = 0 AND TenantId = @tenant, exactly like before.
Turning off just one filter
Here is the part that makes the whole feature worth it. IgnoreQueryFilters now accepts a list of filter names. You pass the names of only the filters you want to skip. Everything you do not name stays switched on.
// Admin report: I want to SEE deleted books,
// but I still must stay inside my own tenant.
var allBooks = await context.Books
.IgnoreQueryFilters([BookFilters.SoftDelete])
.ToListAsync();In this query, the soft-delete rule is off, so deleted books appear. But the tenant rule is still on, so you never leak another customer's data. This is the exact case the old design could not handle cleanly.
Selective ignore in EF Core 10
Steps
Write query
context.Books...
Name filters to skip
IgnoreQueryFilters([SoftDelete])
EF keeps the rest
Tenant filter stays on
Run SQL
WHERE TenantId = @tenant
You can also pass more than one name if you want to drop several at once.
var rawBooks = await context.Books
.IgnoreQueryFilters([BookFilters.SoftDelete, BookFilters.Tenant])
.ToListAsync();And if you call IgnoreQueryFilters() with no arguments, it still turns off every filter, just like before. So your old code keeps working.
Old way vs new way at a glance
| Situation | Before EF Core 10 | EF Core 10 with named filters |
|---|---|---|
| Filters per entity | Only one call to HasQueryFilter | Many calls, each with a name |
| Combining rules | Glue with && into one expression | Keep each rule separate by name |
| Skip all filters | IgnoreQueryFilters() | IgnoreQueryFilters() (still works) |
| Skip just one rule | Not possible | IgnoreQueryFilters(["FilterName"]) |
| Risk of leaking tenant data when ignoring | High (all rules drop together) | Low (keep tenant rule on) |
An important rule: do not mix named and unnamed
You can still create one filter without a name. That behaves exactly like the old days. But there is one rule you must follow:
For a single entity type, use either named filters or one unnamed filter. You cannot use both on the same entity.
So if you want two filters on Book, name both of them. If you try to add a named filter next to an unnamed one on the same entity, EF Core will not allow it. The fix is simple: give every filter a name once you need more than one.
A fuller example: soft delete plus multi-tenancy
Let us put the pieces together in a small, realistic DbContext. This is the shape most teams will actually use.
public class AppDbContext : DbContext
{
private readonly int _currentTenantId;
public AppDbContext(DbContextOptions<AppDbContext> options, ITenant tenant)
: base(options)
{
_currentTenantId = tenant.Id;
}
public DbSet<Book> Books => Set<Book>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.HasQueryFilter(BookFilters.SoftDelete, b => !b.IsDeleted)
.HasQueryFilter(BookFilters.Tenant, b => b.TenantId == _currentTenantId);
}
}Now look at three everyday queries and the SQL idea behind each one.
| Query in C# | Filters that run | Rough SQL WHERE |
|---|---|---|
context.Books.ToListAsync() | Both | IsDeleted = 0 AND TenantId = @t |
context.Books.IgnoreQueryFilters([BookFilters.SoftDelete]) | Tenant only | TenantId = @t |
context.Books.IgnoreQueryFilters() | None | (no filter rows) |
The middle row is the new power. Your admin "recycle bin" page can show deleted books safely, because the tenant boundary never drops.
Three queries, three outcomes
Steps
Normal list
Both filters on
Admin recycle bin
Skip soft delete, keep tenant
Raw data export
Skip all, internal use only
How a query travels through the filters
It can help to picture the steps EF Core takes when you run a query. The named filters sit between your LINQ and the final SQL.
When you call IgnoreQueryFilters([BookFilters.SoftDelete]), EF Core simply skips the "Add SoftDeleteFilter" step and keeps the rest. That is the whole trick, and it is easy to reason about.
Small tips that save you pain
- Use constants for names. A
const string(or a small static class of them) stops typos. A misspelled name will not throw; it just will not match, so the filter you meant to skip stays on, or the one you meant to keep gets confusing. - Name filters as soon as you have two. Do not wait until the combined
&&expression grows messy. Naming early keeps each rule readable and testable. - Never drop the tenant filter by accident. When you reach for
IgnoreQueryFilters()with no arguments, ask yourself if you really want to leave the tenant boundary. In most apps you do not. - Filters need a value at model-building time. Things like the current tenant id are read when the context is created. Make sure that value is set before queries run.
- Filters apply to the root query, not loaded navigations the same way. As before, plan related-data loading carefully; query filters mainly shape the entity you query directly.
A note on migrating older code
If you already have apps on EF Core 8 or 9, moving to named filters is gentle. Your single unnamed filter keeps working with no change at all, because the unnamed form is still supported. You only need to touch the code when you actually want a second filter on the same entity. At that point, give both filters a name and you are done.
A common migration looks like this. You start with one glued filter:
// Old: two rules glued with &&
modelBuilder.Entity<Book>()
.HasQueryFilter(b => !b.IsDeleted && b.TenantId == _currentTenantId);Then you split it into two named filters so each one can be toggled on its own:
// New: two named rules, each independent
modelBuilder.Entity<Book>()
.HasQueryFilter(BookFilters.SoftDelete, b => !b.IsDeleted)
.HasQueryFilter(BookFilters.Tenant, b => b.TenantId == _currentTenantId);The generated SQL for a normal query is the same as before. Nothing slows down. The only thing that changes is your new freedom to skip one rule at a time. Because the change is opt-in, you can do it entity by entity, at your own pace, without a big risky rewrite. Start with the one entity where you most often need an admin or report view, learn the pattern there, and then spread it where it helps.
Why this matters for real apps
Soft delete and multi-tenancy are not rare. They show up in almost every business app: shops, school portals, billing systems, ticket trackers. Before EF Core 10, mixing them with global filters meant either writing fragile manual WHERE clauses or accepting the all-or-nothing switch. Named filters give you a clean middle path. You keep the safety rule (tenant) locked on while flexibly toggling the convenience rule (soft delete) per query. That is a small API change with a big effect on how calm your code feels.
Quick recap
- A global query filter is a rule EF Core adds to every query for an entity, automatically.
- Before EF Core 10, you could have only one filter per entity, and
IgnoreQueryFilters()was all-or-nothing. - EF Core 10 lets you add multiple named filters on one entity using
HasQueryFilter("Name", expression). - You can switch off just one filter with
IgnoreQueryFilters(["Name"]), while the others keep running. - Calling
IgnoreQueryFilters()with no arguments still turns off everything, so old code is safe. - You cannot mix a named filter and an unnamed filter on the same entity. Name all of them when you need more than one.
- Store filter names in constants to avoid silent typo bugs.
- The big win: keep your tenant rule on for safety, while freely toggling soft delete for admin and reporting screens.
References and further reading
- Global Query Filters - EF Core (Microsoft Learn)
- What's New in EF Core 10 (Microsoft Learn)
- IgnoreQueryFilters method reference (Microsoft Learn)
- Named Query Filters in EF 10 by Milan Jovanovic
- Named global query filters in EF Core 10 by Tim Deschryver
- Global Query Filters in EF Core by codewithmukesh
- GitHub issue: Named query filters (dotnet/efcore #8576)
Related Posts
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.
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.
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.
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.