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.
Learning outcomes
Use EF Core 10 named query filters and explain the minimum-version gate.
Compose TenantFilter, SoftDeleteFilter, and ActiveFilter without overwriting previous filters.
Selectively disable only a lifecycle filter while preserving tenant isolation.
Inspect runtime model metadata and generated SQL to verify which policy layers are active.
Design explicit authorization/audit wrappers around filter bypass rather than exposing IgnoreQueryFilters broadly.
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.
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
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.
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
// 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 |
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
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
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.
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
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
- Upgrade the Lesson 1 combined filter to three named filters.
- Seed tenant A/B with active, inactive, and deleted rows.
- Assert ordinary tenant A queries return only active, non-deleted A rows.
-
Disable only
SoftDeleteFilter; verify tenant B never appears. -
Disable only
TenantFilterin an explicitly labeled auditor test; verify the SQL drops the tenant predicate and the authorization gate is required. - 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
- What changed in EF Core 10 for global filters?
- Why is calling unnamed HasQueryFilter multiple times unsafe for composition?
- What should a tenant recycle-bin query disable?
- Why must filter names not come from an HTTP request?
- Do named filters eliminate required-navigation surprises?
- 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
- Global query filters — named/unnamed filter semantics, selective disable, required-navigation behavior
- What's new in EF Core 10 — named filter feature introduction
- IgnoreQueryFilters API — EF Core 10 selective filter-key overload
- EF Core multi-tenancy — tenant values on context and supported patterns
- EF Core testing — integration-testing guidance for database behavior
- EF Core relationships — required/optional relationship semantics that interact with filtered joins