Multi-tenancy in EF Core: no tenant means no rows
The author shows a multi-tenancy pattern for ASP.NET Core and EF Core that uses global query filters so each request only sees its own tenant's rows. When no tenant is set, filtered queries return nothing, which fails closed. The post also covers tenant-scoped unique indexes, limiting IgnoreQueryFilters(), and an integration test that checks cross-tenant access. It is a practical starting point for SaaS backends.
Key points
- Give every tenant-owned entity a shared TenantId and apply it with HasQueryFilter
- With CurrentTenantId as Guid? and no user, the filter matches no rows, so the default is closed
- Include the tenant key in unique indexes such as (TenantId, Phone)
- Limit IgnoreQueryFilters() to sign-in, sign-up and platform jobs and review each call
- An integration test with two tenants catches a missing filter before release
One of the most damaging bugs in a multi-tenant SaaS is a customer seeing another customer's data. Relying on every developer to add a tenant check to each query is fragile. The author moves that check into EF Core's global query filters, so each tenant-owned entity is filtered automatically based on the signed-in user's tenant.
The important design choice is that a missing tenant produces no rows rather than all rows. A forgotten authorization attribute or a background job without a tenant therefore returns an empty result. Unique indexes also include the tenant key, so two businesses can each hold the same phone number. Calls to IgnoreQueryFilters() should stay limited to sign-in, sign-up and platform jobs, and an integration test that has one tenant try to read and modify another tenant's record can catch a missing filter early.
Real-world use
- A multi-business POS or booking SaaS that needs tenant filtering applied to every query automatically
- APIs with anonymous endpoints or background jobs that should return empty results when no tenant is set
- Multi-tenant apps needing per-tenant unique constraints, such as one customer phone number per business
Why it matters
Defining tenant filters once at the DbContext level removes the need to remember them in each query, which lowers the risk of cross-tenant data leaks. A cross-tenant integration test then catches new entities that lack the filter.
Related stories
- Sending leads to Odoo without losing one: a transactional outbox in ASP.NET CoreDEV Community: #aspnetcore2 d ago
- Idempotent NebulaGraph bulk ingestion from .NET using deterministic VIDsDEV Community: #csharp3 d ago
- TableSchema and database structure upgrades: compare, plan, executeDEV Community: #dotnet3 d ago