← Back

EF Core Global Query Filters

2026-01-11 19:39 · 👁 12912

#c##ef core

🔎 EF Core Global Query Filters — powerful… and easy to misuse

If your app has soft deletes, multi-tenancy, or “only active records” rules, you’ll end up repeating the same Where(...) everywhere.

EF Core’s Global Query Filters let you define that rule once, at the model level — and EF applies it automatically to every query.

✅ Typical use cases

  • 🗑️ Soft delete (IsDeleted)
  • 🧑‍🤝‍🧑 Multi-tenant filtering (TenantId)
  • ✅ Active/valid records (IsActive, ValidUntil, etc.)

1) Soft delete with a global filter

public interface ISoftDelete
{
    bool IsDeleted { get; set; }
}

public class Order : ISoftDelete
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }
}
public class AppDbContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>()
            .HasQueryFilter(o => !o.IsDeleted);
    }
}

Now this:

var orders = await db.Orders.ToListAsync();

automatically becomes:

SELECT ... FROM Orders WHERE IsDeleted = 0

2) Multi-tenancy (dynamic per request)

Key idea: the filter can reference DbContext properties.

public class AppDbContext : DbContext
{
    public Guid TenantId { get; }

    public AppDbContext(DbContextOptions options, ITenantProvider tenant)
        : base(options)
        => TenantId = tenant.TenantId;

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>()
            .HasQueryFilter(o => o.TenantId == TenantId);
    }
}

Every query is now tenant-safe by default.


3) When you need to bypass filters

Admin screens, audits, data repair jobs…

var allOrders = await db.Orders
    .IgnoreQueryFilters()
    .ToListAsync();

⚠️ Pitfalls you will hit

1) Filters apply to navigations too

If you include related data, EF applies filters there as well.

var customers = await db.Customers
    .Include(c => c.Orders) // Orders will also be filtered
    .ToListAsync();

This is usually correct… until you’re debugging “missing rows”.

2) Be careful with required relationships

If Order is filtered out, it can affect relationship materialization and results in surprising ways. Sometimes changing required ↔ optional or adjusting your query is needed.

3) Don’t use non-deterministic stuff

Avoid DateTime.Now directly inside the filter (translation + caching issues). Prefer a context property:

public DateTime UtcNow => _clock.UtcNow;
modelBuilder.Entity<Token>()
    .HasQueryFilter(t => t.ExpiresAt > UtcNow);

🧠 Rule of thumb

Global filters are for business invariants — rules that should apply almost always.

If you frequently need exceptions, keep it explicit in queries instead.

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please reload the page.