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

Single Database, Schema-per-Tenant, and Database-per-Tenant Tradeoffs with DbContext Configuration

Compare discriminator-column, schema-per-tenant, and database-per-tenant designs by EF support, migrations, pooling, backup/restore, noisy-neighbor behavior, and operational ownership rather than tenancy slogans.

Advanced180–240 minutestenancy topology 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

Compare discriminator-column, schema-per-tenant, and database-per-tenant topologies using EF support and operational evidence.

02

Explain why schema-per-tenant is not directly supported by EF Core and is impossible on SQLite.

03

Configure a database-per-tenant context without mutating one live context’s connection target.

04

Reason about migration ownership, connection pools, backup/restore, noisy neighbors, and tenant movement.

05

Choose a topology from requirements for isolation, scale, operations, and provider support—not a generic maturity ladder.

06

Keep the free mandatory lab reproducible with SQLite files while clearly separating server-provider behavior.

1. Tenancy topology is an operational architecture decision

Global filters solve one important problem: default row scoping inside a shared model. They do not answer whether every tenant should share the same physical database. ServiceHub now needs an explicit topology contract because backup/restore, maintenance windows, noisy-neighbor risk, connection pools, migrations, and incident blast radius differ more than the LINQ surface suggests.

2. Three common shapes and EF Core support

Topology EF Core support Isolation/operations ServiceHub use
One database + TenantId discriminator Direct via global filters Lowest operational overhead; shared blast radius and capacity Mandatory free lab
Schema per tenant Not directly supported by EF Core Many schemas/migration targets; model-shape/cache complexity Discuss/avoid by default
Database per tenant Direct via per-tenant configuration/connection string Strong backup/restore and blast-radius isolation; many DBs/pools/migrations Free SQLite-file lab + server variant

EF’s own multi-tenancy guidance explicitly marks schema-per-tenant as not supported. SQLite has no schemas at all, which makes the limitation even more concrete for the course baseline.

3. Shared database: simple model, hard security discipline

In the discriminator approach, all work-order rows share one table and every tenant-aware key/index/query must include tenant semantics where required. The database has one migration history and one backup unit. This is operationally efficient but tenant-specific restore is an application/data-recovery exercise rather than a simple database restore.

SQLite · shared-table evidence
SELECT tenant_id, COUNT(*)FROM work_ordersWHERE is_deleted = 0GROUP BY tenant_id;-- One table, one file/database, multiple tenants.

4. Schema per tenant: why “just change ToTable schema” is fragile

Changing ToTable("work_orders", TenantSchema) changes the EF model shape. EF normally caches one model per context type, so dynamic schemas imply model-cache-key customization, separate migration targeting, and potentially unbounded cached models. More importantly, EF Core’s multi-tenancy documentation does not directly support this topology. Do not introduce it just to avoid a tenant column.

C# · illustrative only, not the recommended ServiceHub path
protected override void OnModelCreating(ModelBuilder modelBuilder){    modelBuilder.Entity<WorkOrder>()        .ToTable("work_orders", TenantSchema); // model shape now varies}
Provider boundary

SQLite cannot reproduce schema-per-tenant because SQLite has no schemas. SQL Server/PostgreSQL can have schemas, but provider capability does not make EF migration/model lifecycle simple.

5. Database per tenant: choose the database before creating the context

C# · safe database-per-tenant factory
public sealed class TenantDbContextFactory(    ITenantCatalog catalog,    ILoggerFactory logging){    public ServiceHubContext Create(string authorizedTenantId)    {        var target = catalog.ResolveRequired(authorizedTenantId);        var options = new DbContextOptionsBuilder<ServiceHubContext>()            .UseSqlite(target.ConnectionString)            .UseLoggerFactory(logging)            .Options;        return new ServiceHubContext(options, new FixedTenantContext(authorizedTenantId));    }}

Do not create a shared context and call Database.SetConnectionString while it is in use. Contexts are short-lived units of work; resolve the authorized target first and construct the context for that target.

6. Connection pools and database fan-out

On server providers, connection pools are typically partitioned by connection string. A database-per-tenant estate can therefore create many pools and server sessions, especially when each tenant gets a distinct database/catalog in the connection string. Capacity planning must include number of active tenants, pool limits, failover behavior, credentials, and idle connections. DbContext pooling is a separate mechanism and does not merge driver connection pools.

Concern Shared DB DB per tenant
Migrations One coordinated target N targets; rollout/health state per DB
Backup/restore Whole shared DB; tenant restore is selective Natural per-tenant backup/restore unit
Noisy neighbor Shared CPU/I/O/log/locks Better isolation, still shared server if co-hosted
Connection pools Usually fewer pool keys Potentially many pools/credentials
Tenant move Data copy/partition operation Move/restore database and update catalog

7. Deliberate failure: connection string from untrusted tenant input

C# · WRONG: request controls physical database
var tenant = request.Query["tenant"];var cs = $"Data Source=servicehub-{tenant}.db";var options = new DbContextOptionsBuilder<ServiceHubContext>()    .UseSqlite(cs)    .Options;

Even with sanitized filenames, this lets the request choose a data partition. The repair is an authorization-derived tenant identity plus a server-owned tenant catalog mapping identity to a known connection target. Treat the catalog as security-critical configuration and audit changes to it.

8. Migrations are part of the topology contract

A shared database has one migration history. Database-per-tenant requires a fleet migration strategy: inventory target versions, migrate one coordinated owner, canary a subset, observe, continue, and record failures. Do not run migrations concurrently from every application instance. Schema-per-tenant multiplies this complexity further and is one reason it is not the default EF tenancy pattern.

Deployment manifest · example evidence
tenant-a  servicehub-a.db  schemaVersion=2026082701  status=readytenant-b  servicehub-b.db  schemaVersion=2026082701  status=readytenant-c  servicehub-c.db  schemaVersion=2026082604  status=blocked-review

9. Mandatory free lab: compare shared-file and per-tenant-file SQLite

  1. Create shared.db containing tenants A/B and apply the global filters from Lessons 1–2.
  2. Create tenant-a.db and tenant-b.db with identical schema but only one tenant each.
  3. Record file sizes, migration histories, and backup/restore steps for both shapes.
  4. Demonstrate that a tenant-specific restore is trivial for the per-tenant file and a selective data operation for the shared file.
  5. Record that schema-per-tenant is not reproducible on SQLite and is not directly supported by EF Core.
  6. Delete all disposable files after the lab.

10. Production judgment and bridge

Choose a shared discriminator database when operational simplicity and shared infrastructure dominate and the application/database defenses are strong. Choose database-per-tenant when isolation, tenant-specific restore, regulatory boundaries, or independently scalable storage justify the fleet-management cost. Do not choose schema-per-tenant merely because the engine supports schemas; EF model/migration lifecycle matters. Lesson 4 turns from physical topology to the subtler runtime risk: cached models and pooled contexts carrying tenant-sensitive state.

Check your understanding

  1. Which multi-tenancy topologies does EF Core directly support?
  2. Why is schema-per-tenant problematic in EF Core?
  3. Why must database target selection happen before creating a context?
  4. How does database-per-tenant affect connection pooling?
  5. Which topology makes per-tenant restore simplest?
  6. Why is SQLite useful in this topology lab?
Review the answers

1. Discriminator-column tenancy via global query filters and database-per-tenant via configuration/connection selection.

2. It changes model shape per tenant, complicates model caching/migrations, and EF documentation marks it as not directly supported.

3. A DbContext is a unit of work bound to its configured provider/connection; mutating shared live context configuration risks cross-tenant use and thread-safety failures.

4. Distinct connection strings can create distinct driver pools, increasing pool/session capacity requirements.

5. Database per tenant, because the database itself is the recovery boundary.

6. Separate files model database-per-tenant cheaply and repeatably, while also proving SQLite has no schema-per-tenant feature.

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