← Back to all posts

EF Core: Query Filters

EF Core: Query Filters

Reading Time: 2 minutes

Problem

As applications grow, the same Where clauses get repeated everywhere:

  • IsDeleted == false for soft-delete
  • TenantId == currentTenantId for multi-tenant isolation
  • other visibility rules based on status or ownership

This leads to common issues:

  • missed filters in one endpoint causing data leaks
  • duplicated query logic across services and repositories
  • inconsistent behavior between read paths
  • harder maintenance when rules change

In short, relying on developers to remember critical filters in every query is error-prone.

Solution

Use global query filters with HasQueryFilter so EF Core applies rules automatically to all queries for an entity.

Typical uses:

  • soft-delete filtering
  • tenant isolation
  • default visibility constraints

Also use IgnoreQueryFilters() only when explicitly needed, such as admin screens, audit views, or restore workflows.

Description

1) Soft-delete filter (.NET)

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public bool IsDeleted { get; set; }
}

public class AppDbContext : DbContext
{
    public DbSet<Product> Products => Set<Product>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>()
            .HasQueryFilter(p => !p.IsDeleted);
    }
}

Now every query on Products automatically excludes deleted rows unless overridden.

2) Multi-tenant filter (.NET)

Inject tenant context into your DbContext and use it in the filter.

public interface ITenantProvider
{
    Guid TenantId { get; }
}

public class Order
{
    public int Id { get; set; }
    public Guid TenantId { get; set; }
    public decimal Total { get; set; }
}

public class AppDbContext : DbContext
{
    private readonly ITenantProvider _tenantProvider;

    public AppDbContext(
        DbContextOptions<AppDbContext> options,
        ITenantProvider tenantProvider) : base(options)
    {
        _tenantProvider = tenantProvider;
    }

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

This helps enforce tenant boundaries by default and reduces accidental cross-tenant reads.

3) Combining filters

If you need both soft-delete and tenant rules, combine them in one expression:

modelBuilder.Entity<Order>()
    .HasQueryFilter(o => !o.IsDeleted && o.TenantId == _tenantProvider.TenantId);

4) When to bypass filters

Use IgnoreQueryFilters() intentionally and sparingly:

var allRowsIncludingDeleted = await db.Products
    .IgnoreQueryFilters()
    .ToListAsync();

Good use cases:

  • admin portals
  • forensic/debug tooling
  • restore deleted data

5) Practical cautions

  • Query filters affect every query for that entity, including includes and projections.
  • Test admin/reporting paths that require unfiltered access.
  • Keep filter predicates simple and index-backed for predictable performance.

Summary

EF Core query filters centralize critical data-access rules and make your application safer by default.

You get:

  • less duplicated query logic
  • fewer missed Where conditions
  • stronger tenant and soft-delete consistency

Use HasQueryFilter for defaults and IgnoreQueryFilters() only for explicit exceptional scenarios.

References