Skip to main content
SEMastery
Fundamentalsintermediate

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.

11 min readUpdated February 17, 2026

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.

How a global query filter quietly joins your query before it hits the database

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.

  1. It got messy fast. Add a third or fourth rule and that one line becomes hard to read and hard to test.
  2. 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, calling IgnoreQueryFilters() 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 doBefore EF Core 10With named filters in EF Core 10
Add two filters to one entityMerge them with && into one filterCall HasQueryFilter twice with names
See deleted rows but keep tenant safetyNot possible — IgnoreQueryFilters() removed bothSkip only "SoftDeleteFilter" by name
Read which rules existUntangle one long expressionEach filter has a clear name
Risk of leaking other tenants' dataHigherLower

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.

Two named filters on one entity, both joined into the final query

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

Need all rows?
Skip one rule?
Keep safety rule?

Steps

1

Need all rows?

Call IgnoreQueryFilters() with no name

2

Skip one rule?

Pass that filter's name in a list

3

Keep safety rule?

Leave the tenant filter out of the skip list

A simple decision path for IgnoreQueryFilters in EF Core 10

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 entityAllowed?
One unnamed filterYes
Two named filtersYes
One unnamed + one namedNo (error at model build)
Zero filtersYes

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 same entity serving three different pages, each skipping different named filters

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

User request
Build query
Apply named filters
Run SQL
Return rows

Steps

1

User request

Page asks for orders

2

Build query

LINQ on context.Orders

3

Apply named filters

Active filters joined with AND

4

Run SQL

WHERE clause sent to database

5

Return rows

Only allowed rows come back

How a query flows through named filters before reaching the database

Things to keep in mind

A few small notes will save you trouble.

  • Filters use AND, never OR. All active named filters are combined with AND. If you truly need an OR between two conditions, put that OR inside a single filter expression. Do not expect two separate named filters to act as OR.
  • 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 HasQueryFilter with a name each time.
  • All active filters are joined with AND and added to the SQL WHERE clause, 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

Related Posts