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

Enforce Tenant Isolation in Tests, Database Constraints, Authorization, and Operational Tooling

Turn tenant isolation into defense in depth with adversarial tests, tenant-aware keys and foreign keys, explicit admin capabilities, least-privilege credentials, optional database row-level security, and auditable bypass paths.

Advanced180–240 minutestenant red-team 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

Design adversarial tenant-isolation tests for reads, writes, bypasses, background jobs, and admin tooling.

02

Add tenant-aware relational keys/foreign keys so cross-tenant relationships fail at the database boundary.

03

Keep tenant identity server-controlled on inserts/updates and reject over-posted TenantId values.

04

Separate normal and administrative capabilities and audit every cross-tenant/bypass operation.

05

Explain SQLite’s database-enforcement limits and optional SQL Server row-level security as a second data-access boundary.

06

Build a production runbook that checks authorization, least privilege, migration state, bypass telemetry, and recovery procedures.

1. Assume one layer will eventually be bypassed

Global filters can be disabled. A privileged admin tool can be misconfigured. A background job can construct the wrong context. Raw SQL can bypass EF entirely. Defense in depth means a single coding mistake should not automatically become a cross-tenant incident.

For ServiceHub, the enforcement stack is: trusted tenant resolution → authorization → EF filter defaults → tenant-aware writes → relational constraints → least-privilege database access → optional database row-level security where supported → adversarial tests and audit.

2. Tenant identity is server-owned during writes

C# · command DTO excludes TenantId
public sealed record CreateWorkOrderCommand(    string WorkOrderNumber,    string Summary,    string Street,    string City,    string Region,    string PostalCode,    string CountryCode);// Add this overload inside the existing WorkOrder partial class.public static WorkOrder OpenForTenant(    string tenantId, string number, string summary, ServiceAddress address){    var row = Open(number, summary, address);   // established Chapter 06 factory    row.TenantId = tenantId;    return row;}public async Task<int> HandleAsync(CreateWorkOrderCommand command, CancellationToken ct){    var address = new ServiceAddress(        command.Street, command.City, command.Region,        command.PostalCode, command.CountryCode);    var row = WorkOrder.OpenForTenant(        db.TenantId, command.WorkOrderNumber, command.Summary, address);    db.Add(row);    await db.SaveChangesAsync(ct);    return row.Id;}

If an external DTO contains TenantId, treat it as a resource-selection claim to validate against authorization, never as the persisted truth. Better APIs often omit it entirely for tenant-local operations.

3. Database constraints should understand tenant ownership

A plain foreign key from work_orders.assigned_technician_id to technicians.id can permit a work order in tenant A to reference a technician in tenant B if IDs are globally valid. This lesson therefore extends the tenancy migration to add/backfill a required tenant_id on technicians, then strengthens the relationship by including the tenant discriminator in the principal and foreign key. Review that migration like any other ownership backfill; do not assign tenants with a blanket production default.

C# · tenant-aware principal key and FK
modelBuilder.Entity<Technician>(b =>{    b.HasAlternateKey(t => new { t.TenantId, t.Id });});modelBuilder.Entity<WorkOrder>(b =>{    b.HasOne(w => w.AssignedTechnician)        .WithMany(t => t.WorkOrders)        .HasForeignKey(w => new { w.TenantId, w.AssignedTechnicianId })        .HasPrincipalKey(t => new { t.TenantId, t.Id });});
SQLite · representative composite constraint
FOREIGN KEY (tenant_id, assigned_technician_id)REFERENCES technicians (tenant_id, technician_id)

This does not implement row-level authorization, but it blocks an important class of cross-tenant relationship corruption even if EF filters are bypassed.

4. Adversarial tests: prove the negative cases

C# · representative tenant-isolation integration tests
[Fact]public async Task TenantA_cannot_read_TenantB_row(){    await using var a = factory.CreateForTenant("tenant-a");    var visible = await a.WorkOrders.AsNoTracking().Select(w => w.TenantId).Distinct().ToListAsync();    Assert.Equal(new[] { "tenant-a" }, visible);}[Fact]public async Task Cross_tenant_technician_assignment_fails_constraint(){    // Seed A work order + B technician, then deliberately set the wrong FK in a low-level test.    await Assert.ThrowsAsync<DbUpdateException>(() => attacker.SaveChangesAsync());}[Fact]public async Task Recycle_bin_keeps_TenantFilter(){    var sql = db.WorkOrders        .IgnoreQueryFilters(new[] { "SoftDeleteFilter" })        .ToQueryString();    Assert.Contains("tenant_id", sql, StringComparison.OrdinalIgnoreCase);}

Also test raw SQL helpers, scheduled jobs, message consumers, exports, support/admin endpoints, and migration/backfill scripts. The dangerous code is often outside the normal request handler.

5. Deliberate failure: admin equals “filters off everywhere”

C# · WRONG: privilege is implicit and unaudited
if (user.IsAdmin)    query = query.IgnoreQueryFilters();

“Admin” is too broad. Split capabilities: tenant recycle-bin read, tenant restore, cross-tenant support lookup, compliance export, and migration repair are different privileges. Cross-tenant operations should require a break-glass or explicit operational role, a reason/ticket, and an audit record.

6. SQLite: strong constraints, no built-in row-level security policy

The mandatory free lab uses SQLite, which can enforce foreign keys, uniqueness, checks, and transactions, but does not provide SQL Server-style row-level security policies. That means a shared-database SQLite application credential can still read any row through raw SQL. Application authorization/filtering and constrained operational tooling remain essential.

SQLite · verify relational enforcement is actually enabled
PRAGMA foreign_keys;PRAGMA foreign_key_list('work_orders');PRAGMA index_list('work_orders');

7. Optional SQL Server defense: row-level security

SQL Server and Azure SQL support row-level security (RLS) with filter and block predicates. A common application pattern stores the authorized tenant in SESSION_CONTEXT after opening a connection, and an RLS security policy filters/blocks rows by tenant_id. This is an additional database boundary, not a reason to remove EF filters or authorization.

SQL Server · conceptual RLS policy
CREATE FUNCTION Security.fn_tenantPredicate(@TenantId nvarchar(64))RETURNS TABLE WITH SCHEMABINDINGAS RETURN SELECT 1 AS allowedWHERE @TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS nvarchar(64));GOCREATE SECURITY POLICY Security.WorkOrderTenantPolicyADD FILTER PREDICATE Security.fn_tenantPredicate(tenant_id) ON dbo.work_orders,ADD BLOCK PREDICATE Security.fn_tenantPredicate(tenant_id) ON dbo.work_orders AFTER INSERTWITH (STATE = ON);GO

Connection pooling matters: session state is attached to the physical connection. The application must set/verify it for every opened connection according to the provider’s documented semantics. Test the policy with the actual production driver and pooling configuration.

8. Least privilege and operational tooling

Actor Normal capability Explicitly denied/controlled
Application runtime Tenant-scoped CRUD DDL/migrations, arbitrary cross-tenant export
Migration owner Reviewed DDL/data migrations Normal request traffic
Support tool Narrow audited tenant lookup Unrestricted IgnoreQueryFilters
Background worker Assigned tenant/partition or explicit fleet scope Implicit tenant from stale pooled state

Separate runtime and migration credentials where feasible. A query filter cannot protect against a database principal with broad direct table access, and a database policy cannot validate an application user’s business authorization unless identity is safely propagated.

9. Production telemetry and incident runbook

Security telemetry should answer: How often are query filters disabled? Which capability initiated it? Were cross-tenant result sets expected? Are there tenant-mismatch constraint failures? Are contexts ever created without tenant scope? Did a tenant catalog mapping change? Keep tenant IDs out of high-cardinality public metrics but include them in protected audit records when incident response requires it.

Runbook steps: disable the affected capability, identify tenant/time window, preserve logs, verify database constraints/RLS state, rotate credentials if direct access was involved, compare audit export counts, restore/correct data if needed, then add a regression test for the exact bypass path.

10. Mandatory final lab: red-team the boundary

  1. Seed two tenants with overlapping human-readable work-order numbers so a missing tenant predicate cannot hide behind globally unique data.
  2. Run normal reads, recycle-bin reads, direct key lookups, and projections under both tenant contexts.
  3. Attempt a cross-tenant technician assignment and assert the composite foreign key rejects it.
  4. Attempt an unauthorized TenantFilter bypass through the application service; assert authorization fails before SQL executes.
  5. Run an intentionally privileged cross-tenant audit query, emit an audit event, and verify the result set is labeled/handled as multi-tenant data.
  6. On SQLite, record the absence of RLS and verify PRAGMA foreign_keys=1. Optionally repeat on SQL Server with RLS in a disposable database.
  7. Document recovery and credential ownership, then delete the disposable lab databases.

11. Production judgment and bridge

Tenant isolation is a system property, not an EF feature. Global and named filters create excellent safe defaults; relational constraints preserve tenant-consistent relationships; authorization controls who may bypass; database policies can add another boundary on capable providers; and tests/audit make violations observable. Chapter 23 turns this philosophy into a broader EF testing strategy: deciding what can be unit-tested and what must execute against a real relational provider.

Check your understanding

  1. Why should TenantId usually be omitted from tenant-local command DTOs?
  2. What does a composite tenant-aware foreign key prevent?
  3. Does SQLite provide SQL Server-style row-level security?
  4. Why is “admin means IgnoreQueryFilters()” unsafe?
  5. What is the role of SQL Server RLS in this design?
  6. What makes an isolation test adversarial?
Review the answers

1. The server already knows the authorized tenant; accepting a client-supplied value creates an unnecessary over-posting/resource-selection risk.

2. A dependent row in one tenant from referencing a principal row belonging to another tenant.

3. No. SQLite can enforce relational constraints but not a built-in per-row security policy equivalent to SQL Server RLS.

4. It creates an overly broad, unaudited capability. Specific bypass purposes should be separately authorized and logged.

5. An optional additional database-tier boundary using filter/block predicates; it complements rather than replaces EF/application authorization.

6. It deliberately attempts cross-tenant reads/writes, bypasses, background/admin paths, and low-level database operations—not only happy-path tenant queries.

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