Chapter 22 · Global Query Filters, Multi-Tenancy, Soft Delete, and Data Partitioning

Named Query Filters in EF Core 10: Selective Disabling and Composable Policy Layers

Compose tenant, soft-delete, and active-row policies with EF Core 10 named query filters, selectively disable only the intended layer, and test the generated SQL and authorization consequences of every bypass.

Advanced180–240 minutesnamed-filter policy labEF Core 10.0.11 · Microsoft.EntityFrameworkCore.Sqlite 10.0.11 · dotnet-ef 10.0.11 · .NET 10.0.11 · SDK 10.0.400Mandatory free local SQLite path · SQL Server RLS optional in Lesson 5Tenancy/security behavior reviewed: August 27, 2026

Learning outcomes

01

Use EF Core 10 named query filters and explain the minimum-version gate.

02

Compose TenantFilter, SoftDeleteFilter, and ActiveFilter without overwriting previous filters.

03

Selectively disable only a lifecycle filter while preserving tenant isolation.

04

Inspect runtime model metadata and generated SQL to verify which policy layers are active.

05

Design explicit authorization/audit wrappers around filter bypass rather than exposing IgnoreQueryFilters broadly.

06

Contrast EF10 named filters with the pre-EF10 combined-predicate pattern.

1. The operational need: restore deleted data without becoming cross-tenant admin

Lesson 1 showed why all-or-nothing IgnoreQueryFilters() is too blunt. A tenant administrator may legitimately need to view deleted work orders for that tenant, while remaining unable to see another tenant’s rows. EF Core 10 named filters let the model express those policy layers separately.

Version gate

Named global query filters and selective disabling are EF Core 10 features. On EF Core 9 and earlier, combine predicates into one filter and accept that IgnoreQueryFilters disables the combined filter as a whole.

2. Configure three independently named policy layers

C# · EF Core 10 named filters
modelBuilder.Entity<WorkOrder>()    .HasQueryFilter("TenantFilter", w => w.TenantId == TenantId)    .HasQueryFilter("SoftDeleteFilter", w => !w.IsDeleted)    .HasQueryFilter("ActiveFilter", w => w.IsActive);

Calling unnamed HasQueryFilter repeatedly does not compose policies; the later unnamed filter replaces the earlier one. Naming each filter is what makes the policy layers independently addressable in EF Core 10.

C# · WRONG on an EF10 model if you intended composition
builder.HasQueryFilter(w => w.TenantId == TenantId);builder.HasQueryFilter(w => !w.IsDeleted); // overwrites the prior unnamed filter

3. Ordinary, recycle-bin, and auditor queries have different capability sets

C# · three intentionally different paths
// Normal tenant UI: all filters remain active.var current = await db.WorkOrders.ToListAsync(ct);// Tenant recycle bin: disable only lifecycle filters; tenant remains active.var recycleBin = await db.WorkOrders    .IgnoreQueryFilters(new[] { "SoftDeleteFilter", "ActiveFilter" })    .Where(w => w.IsDeleted)    .ToListAsync(ct);// Break-glass auditor: disabling TenantFilter requires a separate authorization path.var crossTenant = await db.WorkOrders    .IgnoreQueryFilters(new[] { "TenantFilter" })    .ToListAsync(ct);

The API does not know whether a filter name is “security-sensitive.” Application authorization must decide who can disable TenantFilter. The model only supplies the mechanism.

4. Generated SQL is your policy diff

Query Expected predicates
Normal tenant_id + NOT is_deleted + is_active
Recycle bin tenant_id remains; lifecycle predicates disabled; explicit is_deleted predicate may be added
Cross-tenant auditor tenant predicate absent; lifecycle predicates still present unless separately disabled
SQLite · recycle-bin shape
SELECT ...FROM "work_orders" AS "w"WHERE "w"."tenant_id" = @__ef_filter__TenantId_0  AND "w"."is_deleted"

Capture these shapes in integration tests. A test that merely asserts “recycle bin returned one row” can pass while the tenant predicate is accidentally missing. Assert both behavior and SQL/log evidence where the security risk justifies it.

5. Make bypass a capability, not a convenience extension method

C# · explicit tenant-admin query service
public sealed class WorkOrderRecycleBin(ServiceHubContext db, IAuthorizationGate auth){    public async Task<IReadOnlyList<DeletedWorkOrderDto>> ListAsync(        CancellationToken ct)    {        auth.Require("workorders.recycle-bin.read");        return await db.WorkOrders            .IgnoreQueryFilters(new[] { "SoftDeleteFilter", "ActiveFilter" })            .Where(w => w.IsDeleted)            .OrderBy(w => w.Id)            .Select(w => new DeletedWorkOrderDto(w.Id, w.WorkOrderNumber, w.Summary))            .AsNoTracking()            .ToListAsync(ct);    }}

Notice what the service does not accept: there is no caller-supplied tenant ID and no arbitrary list of filter names. Tenant scope came from trusted context state. The capability is specific, reviewable, and easy to audit.

6. Deliberate failure: generic “disable filters” endpoint

C# · WRONG: caller chooses policy keys
app.MapGet("/admin/workorders", async (    string[] disable,    ServiceHubContext db,    CancellationToken ct) =>{    return await db.WorkOrders        .IgnoreQueryFilters(disable)        .ToListAsync(ct);});

This converts internal model-policy names into an externally controllable privilege API. An attacker who reaches the endpoint can request TenantFilter. The repair is fixed server-side capabilities with authorization and audit events; filter names never come from the request.

7. Required-navigation behavior still applies to named filters

Naming filters does not alter SQL join semantics. If Technician later receives an ActiveTechnicianFilter and a work-order relationship is configured as required, Include/query shapes may use inner joins and omit work orders whose technician is filtered. The filter names make selective testing easier, but the relational model must still be correct.

C# · compare filter-on/filter-off shapes
var normal = db.WorkOrders.Include(w => w.AssignedTechnician);var diagnostic = db.WorkOrders    .IgnoreQueryFilters(new[] { "ActiveTechnicianFilter" })    .Include(w => w.AssignedTechnician);Console.WriteLine(normal.ToQueryString());Console.WriteLine(diagnostic.ToQueryString());

8. Pre-EF10 fallback for maintenance branches

C# · EF Core 9 and earlier pattern
builder.HasQueryFilter(w =>    w.TenantId == TenantId &&    !w.IsDeleted &&    w.IsActive);

The combined predicate remains useful on older supported majors, but selective disabling is not available. If a maintenance branch needs a recycle bin, write an explicitly tenant-scoped raw/query path or upgrade deliberately; do not pretend the EF10 overload exists on EF9.

9. Mandatory lab: verify every filter combination

  1. Upgrade the Lesson 1 combined filter to three named filters.
  2. Seed tenant A/B with active, inactive, and deleted rows.
  3. Assert ordinary tenant A queries return only active, non-deleted A rows.
  4. Disable only SoftDeleteFilter; verify tenant B never appears.
  5. Disable only TenantFilter in an explicitly labeled auditor test; verify the SQL drops the tenant predicate and the authorization gate is required.
  6. Record runtime EF Core package version; run the same project against an EF9 branch only as a compile-failure/version-gate demonstration.

10. Production judgment and bridge

Named filters are valuable because they make policy composition explicit, not because they make bypass safe. Keep security-sensitive names centralized as constants, hide bypass behind capabilities, and test SQL/result boundaries. Lesson 3 zooms out from row-level discriminator tenancy to schema/database topology and operational isolation.

Check your understanding

  1. What changed in EF Core 10 for global filters?
  2. Why is calling unnamed HasQueryFilter multiple times unsafe for composition?
  3. What should a tenant recycle-bin query disable?
  4. Why must filter names not come from an HTTP request?
  5. Do named filters eliminate required-navigation surprises?
  6. What is the pre-EF10 fallback?
Review the answers

1. Multiple filters can be named on one entity type and selected filter names can be disabled independently.

2. A later unnamed call overwrites the previous unnamed filter instead of adding another independently managed policy.

3. Only the lifecycle filters it needs to bypass; TenantFilter should remain active.

4. They are internal policy controls; caller-selected names can become privilege escalation, including disabling tenant isolation.

5. No. Requiredness and join shape still determine whether filtered related rows remove parents.

6. Use a single combined predicate and design special privileged queries explicitly because selective filter disabling is unavailable.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n