Global Query Filters: Automatic WHERE Clauses in EF Core

Certain filtering requirements cut across your entire application — soft deletes, multi-tenancy, active record flags. Sprinkling .Where(x => !x.IsDeleted) on every query is tedious and error-prone. EF Core's global query filters let you define these filters once at the model level and have them applied automatically to every query.

Defining a Global Query Filter

Filters are configured in OnModelCreating using HasQueryFilter:

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

Now every query against Products — whether via LINQ, Find(), or navigation properties — automatically includes WHERE IsDeleted = 0.

Example.cs
// Both of these apply the filter automatically
var products = await context.Products.ToListAsync();
var product = await context.Products.FindAsync(5);

Soft Deletes Pattern

A complete soft delete implementation with global filters:

Example.cs
public interface ISoftDeletable
{
    bool IsDeleted { get; set; }
    DateTime? DeletedAt { get; set; }
}

public class Product : ISoftDeletable
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
}

Apply the filter to all entities that implement the interface:

Example.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        if (typeof(ISoftDeletable).IsAssignableFrom(entityType.ClrType))
        {
            var parameter = Expression.Parameter(entityType.ClrType, "e");
            var property = Expression.Property(parameter, nameof(ISoftDeletable.IsDeleted));
            var filter = Expression.Lambda(Expression.Not(property), parameter);

            modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filter);
        }
    }
}

Instead of deleting records, mark them:

Example.cs
public async Task SoftDeleteAsync(int productId)
{
    var product = await context.Products.FindAsync(productId);
    if (product is not null)
    {
        product.IsDeleted = true;
        product.DeletedAt = DateTime.UtcNow;
        await context.SaveChangesAsync();
    }
}

Multi-Tenancy with Query Filters

Global filters can reference properties on the DbContext itself, making them ideal for multi-tenancy:

TenantDbContext.cs
public class TenantDbContext : DbContext
{
    private readonly int _tenantId;

    public TenantDbContext(DbContextOptions options, ITenantProvider tenantProvider)
        : base(options)
    {
        _tenantId = tenantProvider.GetCurrentTenantId();
    }

    public DbSet<Invoice> Invoices => Set<Invoice>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Invoice>()
            .HasQueryFilter(i => i.TenantId == _tenantId);
    }
}

The filter captures the _tenantId field by reference. EF Core evaluates it each time a query is executed, so it correctly resolves to the current tenant for each request.

Bypassing Filters

Sometimes you need to see everything — admin panels, data migrations, reporting. Use IgnoreQueryFilters():

Example.cs
// See all products including soft-deleted ones
var allProducts = await context.Products
    .IgnoreQueryFilters()
    .ToListAsync();

This disables all global filters on the entity. You cannot selectively ignore one filter while keeping another. If you need that level of control, consider splitting concerns across different DbContext types.

Filters and Navigation Properties

Global filters apply to navigation properties too. If Order has a collection of OrderItem entities, and OrderItem has a query filter, then loading order.Items will automatically exclude filtered items:

Example.cs
modelBuilder.Entity<OrderItem>()
    .HasQueryFilter(i => !i.IsCancelled);

// Loading an order's items respects the filter
var order = await context.Orders
    .Include(o => o.Items)
    .FirstAsync(o => o.Id == 1);
// order.Items only contains non-cancelled items

Limitations and Gotchas

One filter per entity. Each entity type can have only one query filter. If you need to combine soft delete and tenancy, merge them into a single expression:

Example.cs
modelBuilder.Entity<Invoice>()
    .HasQueryFilter(i => !i.IsDeleted && i.TenantId == _tenantId);

Performance. Filters add WHERE clauses to every query. Ensure the filtered columns are indexed, particularly for high-volume tables:

Example.cs
modelBuilder.Entity<Product>()
    .HasIndex(p => p.IsDeleted);

IgnoreQueryFilters is all-or-nothing. You cannot selectively disable one filter while keeping others active on the same entity.

When to Use Global Filters

Global query filters shine when the filtering logic is truly universal — soft deletes, tenant isolation, active/inactive flags. They prevent entire categories of bugs where a developer forgets to add a filter clause. For ad-hoc or conditional filtering, stick with regular .Where() calls in your queries.