Resources / Field guide
Multi-tenancy with EF Core global query filters
One database, many customers, and an absolute requirement that no tenant ever sees another's data. EF Core's global query filters make that the default behavior instead of something you have to remember on every query. Here's the pattern, how to add a super-admin that can see across tenants, and the traps that turn a clean design into a data leak.
The core problem
In a shared-database multi-tenant app, every tenant-owned table has a CompanyId, and every query must filter by the current tenant. Doing that by hand on every query is a time bomb — one forgotten .Where() and tenant A sees tenant B's records. The fix is to make isolation the default, enforced once, centrally.
The pattern: a global query filter
EF Core lets you attach a filter to an entity type that's automatically applied to every query against it. Wire it in OnModelCreating, reading the current tenant from a scoped service:
protected override void OnModelCreating(ModelBuilder b)
{
b.Entity<Invoice>().HasQueryFilter(x =>
x.DeletedAt == null &&
(_current.IsSuperAdmin || x.CompanyId == _current.CompanyId));
// …same filter shape on every tenant-scoped entity
}
Now a plain _db.Invoices.ToList() only ever returns the current tenant's rows. Developers stop thinking about tenant scoping because the framework won't let them forget it. (The same filter also folds in soft-delete — DeletedAt == null — so deleted rows vanish from normal queries too.)
The super-admin that spans tenants
Platform operators need to see across all tenants. Rather than a parallel set of unfiltered queries, fold it into the same filter with a flag on the current-user context:
// When the signed-in principal is a platform super-admin,
// IsSuperAdmin short-circuits the CompanyId check…
(_current.IsSuperAdmin || x.CompanyId == _current.CompanyId)
When a super-admin logs in, their token carries is_super_admin = true, the filter's left side is true, and every tenant-scoped query returns rows across all tenants, pooled together. One code path, no second query surface to keep in sync.
Reads and writes are not symmetric for a super-admin. Reads see every tenant merged — but anything the super-admin creates is still scoped to its own CompanyId, and per-tenant sequences (invoice numbers, settings) resolve against that company. To edit within a specific tenant, use a real admin account in that tenant. Treat the super-admin as read-across, write-local, and design your UI so it's obvious which tenant an action will affect.
Don't ship the seeded admin. Bootstrapping a platform usually means a seeded super-admin user — and a hardcoded seed password that often ends up logged in plaintext on first boot. That's fine for local dev and a critical hole in production. Rotate it on first deploy (or better, force a reset), and never let the seed credentials reach a live environment as-is.
More traps worth knowing
- Identifiers aren't unique across tenants. A human-friendly number (order #613) can exist in more than one tenant. When a super-admin views pooled data, always show which tenant a record belongs to, or you'll act on the wrong one.
- The filter can be bypassed.
IgnoreQueryFilters()disables it for a query — useful for legitimate cross-tenant admin reads, dangerous if misused. Keep those call sites few and reviewed. - The tenant must be on the context. The current tenant ID has to be available to the
DbContext(resolved at login, carried in the request scope) before any query runs, or the filter has nothing to compare against. - Defense in depth. For truly sensitive isolation, back the app-layer filter with database Row-Level Security so a bug in code can't leak across tenants.
When this fits
Shared-database multi-tenancy with query filters is the right default for most B2B SaaS: one schema, low operational overhead, strong isolation when done carefully. If a tenant demands physical data separation (regulatory, or a whale customer), you move them to a dedicated database — but the filter pattern still earns its keep for everyone else.
Building or hardening a multi-tenant SaaS?
I design tenant isolation, auth, and the data model that keeps customers' data truly separate — and I'll pressure-test it for leaks. Let's talk about your platform.
Work with me