Multi-Tenant SaaS .NET products face a deceptively simple question: where does tenant data live? One shared database keeps operations cheap and upgrades uniform. A dedicated database per tenant isolates each customer completely but multiplies migrations, backups, and infrastructure cost. Between them sits schema-per-tenant, which separates tables without splitting the server. This guide compares the three models and shows how each one is implemented in EF Core 10, so you can choose with your eyes open.
The Three Isolation Models in Multi-Tenant SaaS .NET
Every isolation model is a point on the same trade-off curve. The horizontal axis is operational efficiency, the vertical axis is isolation strength, and the cost of moving right is multiplied migrations, multiplied backups, and multiplied deployment complexity. The right model depends on how many tenants you have, how sensitive their data is, and what your pricing tiers will support.
- Row-level tenant column: one database, one schema, one TenantId column, one global query filter
- Schema per tenant: one database, one schema per tenant, shared server resources
- Database per tenant: one database per customer, complete isolation, highest cost
Row-Level Tenant Column: The Default Choice
The row-level model keeps a single database and adds a TenantId column to every tenant-owned table. A global query filter then appends WHERE TenantId = @current to every query, and the write side sets TenantId when an entity is created. This is the cheapest model to operate: one set of migrations, one backup, and one connection pool that serves all tenants efficiently.
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Invoice>().HasQueryFilter(
i => i.TenantId == _tenant.Id);
builder.Entity<Invoice>().HasIndex(
i => new { i.TenantId, i.Number });
}
The trade-off is that every team member must treat the filter as load-bearing. A bypass such as IgnoreQueryFilters or a handwritten SQL statement is a data breach waiting to happen. The defense is structural: require TenantId on writes, index TenantId in composite keys, and run automated isolation tests that verify tenant A can never read tenant B rows.
A shared pool of connections also keeps cold starts low, because one warm database serves every tenant. The same table structures mean analytics queries can run across all tenants with a single join, which is a decisive advantage when your roadmap includes cross-tenant reporting.
Schema-per-Tenant: Clean Separation, One Database
The schema-per-tenant model keeps a single database but gives each tenant its own schema, such as tenant_acme and tenant_globex. Tables stay physically separate while the server is shared. In EF Core you set the default schema per DbContext instance, which makes the model a natural extension of the tenant-aware pattern: each tenant's context targets its own schema.
optionsBuilder.UseSqlServer(connectionString, sql =>
{
sql.MigrationsHistoryTable(
"__EFMigrationsHistory", $"tenant_{tenantId}");
});
modelBuilder.HasDefaultSchema($"tenant_{tenantId}");
Schema-per-tenant is a middle ground that many SaaS teams like because a bug in one schema cannot directly touch another tenant's tables. The cost is in migrations: every schema needs the migration applied, and EF Core's migration history table has to be schema-aware, which complicates CI. It is best suited to a smaller number of larger tenants.
Database-per-Tenant: Maximum Isolation, Maximum Cost
The database-per-tenant model provisions a full database for every customer. It is the strongest isolation available: a compromise in the application cannot cross database boundaries, and backup, restore, retention, and index tuning can all be tailored per tenant. It is the standard answer for enterprise contracts with strict compliance requirements.
The implementation needs a tenant store that maps a tenant to a connection string. The factory below reads the tenant name from the TenantContext and builds an AppDbContext bound to the right database.
public static class TenantDbFactory
{
public static AppDbContext Create(TenantContext tenant, IConfiguration config)
{
var connection = config.GetConnectionString(tenant.Name)
?? throw new InvalidOperationException("Unknown tenant.");
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer(connection)
.Options;
return new AppDbContext(options, tenant);
}
}
The cost is real. Each tenant adds a migration run, a backup job, and a monitoring surface, so the model stops scaling economically past a few dozen or a few hundred tenants. That is why it is usually a premium tier rather than the default.
Choosing a Model for Your Multi-Tenant SaaS .NET App
Start with the row-level model unless a concrete requirement pushes you elsewhere. Move to schema-per-tenant when tenants demand visible table separation. Move to database-per-tenant when compliance, data residency, or retention requirements make shared storage unacceptable. Because EF Core separates the query logic from the storage strategy, you can support multiple models at once by choosing the DbContext implementation per tenant.
- Few tenants and strict compliance needs: database per tenant
- Many tenants and commodity pricing: row-level tenant column
- Fewer, larger tenants that want separation without new servers: schema per tenant
- Premium tiers that pay extra: any model, selected per tenant
Key Takeaways
- Row-level tenant column is the cheapest default and works for most SaaS pricing
- Schema-per-tenant separates tables while sharing the server and adds migration complexity
- Database-per-tenant offers maximum isolation at multiplied operational cost
- EF Core lets you select the isolation model per tenant at runtime
- Indotalent's SaaS CRM and HRM ship the full multi-tenant database architecture as .NET 10 source code
FAQ
Is a shared database insecure? No. With a global query filter and TenantId enforced on writes, isolation is structural. Treat filter bypasses as the actual security boundary.
Can tenants be upgraded to a stronger model later? Yes. A data migration job can move a tenant's rows from the shared schema into their own schema or database with no downtime for other tenants.
When does database-per-tenant actually pay off? When tenants are few and large, when compliance requires physical separation, or when customers demand tenant-specific retention and indexing.
Does schema-per-tenant work with EF Core migrations? Yes, but each schema needs its own migration history. Automate applying migrations across schemas in CI or keep the tenant count small.