Why Multi‑Tenant Architecture Matters for Modern SaaS
Imagine a startup that launches a subscription‑based analytics platform and needs to support 10,000 customers within the first six months. A single‑tenant deployment would require 10,000 separate instances, exploding operational cost and maintenance overhead. Multi‑tenant design consolidates those customers into a shared codebase and database layer while preserving data isolation, enabling predictable scaling and lower per‑customer price.
Choosing .NET 8 as the Foundation
.NET 8, released in November 2023, introduces native support for minimal APIs, improved AOT compilation, and built‑in diagnostics that are ideal for high‑density SaaS workloads. The new Microsoft.AspNetCore.Http.HttpContext extensions let you inject tenant identifiers early in the request pipeline, reducing boilerplate. For example, a middleware that reads a sub‑domain and stores it in HttpContext.Items can be written in less than 30 lines.
public class TenantResolutionMiddleware
{ private readonly RequestDelegate _next;
public TenantResolutionMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{ var host = context.Request.Host.Host;
var tenantId = host.Split('.').FirstOrDefault(); // assumes tenant.myapp.com
if (!string.IsNullOrEmpty(tenantId))
{ context.Items["TenantId"] = tenantId;
}
await _next(context);
}
} Register the middleware in Program.cs with app.UseMiddleware<TenantResolutionMiddleware>(); and every downstream service can retrieve the tenant ID without repeating parsing logic.
Database Strategy: Azure SQL Elastic Pools
Azure SQL Elastic Pools allow you to host dozens of databases—each representing a tenant—while sharing a common set of compute resources. The pool automatically balances DTU or vCore consumption across active databases, which is perfect for SaaS where some tenants are idle and others spike during reporting periods. Microsoft reports up to 80 % cost reduction compared to provisioning individual single‑tenant databases at peak capacity.
When you create a pool, you define a maximum eDTU (elastic DTU) and a per‑database cap. For a SaaS with 2,000 tenants, a pool of 5,000 eDTU and a per‑database limit of 25 eDTU often suffices, letting the most demanding tenant use up to 25 eDTU while the pool absorbs occasional bursts.
EF Core Model Design for Tenant Isolation
EF Core 8 adds HasQueryFilter support for global filters, which you can tie to the tenant ID stored in HttpContext. Define a base entity that includes TenantId and apply the filter in OnModelCreating:
public abstract class TenantEntity
{ public string TenantId { get; set; }
}
public class Order : TenantEntity
{ public int Id { get; set; }
public decimal Amount { get; set; }
}
public class AppDbContext : DbContext
{ private readonly IHttpContextAccessor _httpAccessor;
public AppDbContext(DbContextOptions<AppDbContext> options, IHttpContextAccessor httpAccessor)
: base(options) => _httpAccessor = httpAccessor;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{ var tenantId = _httpAccessor.HttpContext?.Items["TenantId"] as string;
modelBuilder.Entity<Order>().HasQueryFilter(o => o.TenantId == tenantId);
}
} This approach guarantees that every LINQ query automatically includes the tenant predicate, eliminating accidental cross‑tenant data leaks. For write operations, set TenantId in SaveChangesAsync overrides.
Connection Management and Elastic Pool Scaling
Each tenant’s connection string points to its dedicated database within the pool. Store the mapping in a lightweight configuration service (Azure App Configuration or a Redis cache). When a new tenant signs up, automate database provisioning with Azure CLI:
az sql db create \
--resource-group MySaaS-rg \
--server mysaas-sql \
--name tenant_12345 \
--elastic-pool MySaaSPool \
--collation SQL_Latin1_General_CP1_CI_AS Because the pool already allocates compute, creating a new database takes seconds and costs only storage. Monitor pool utilization via Azure Monitor; set alerts at 80 % eDTU usage to trigger automatic scaling of the pool’s eDTU quota.
Performance Tips and Real‑World Numbers
A case study from a European fintech SaaS showed that moving from 200 single‑tenant Azure SQL databases to a 3,000‑eDTU elastic pool cut monthly DB costs from $12,000 to $4,500 while maintaining sub‑200 ms query latency for 95 % of requests. Key tactics included:
- Enabling
Read‑Committed Snapshot Isolationto reduce lock contention. - Using compiled queries in EF Core for frequent lookup patterns (e.g.,
dbContext.Orders.FirstOrDefaultAsync(o => o.Id == id)). - Implementing a tenant‑specific connection pool via
Microsoft.Data.SqlClientconnection string pooling.
Testing Multi‑Tenant Logic Locally
During development, you can simulate the pool with a single local Azure SQL instance and create separate databases named tenant_dev_1, tenant_dev_2, etc. Use Docker to spin up mcr.microsoft.com/mssql/server:2022-latest and connect with Server=localhost,1433;User Id=sa;Password=YourStrong!Pass;. The same EF Core filter logic works unchanged, giving confidence before pushing to Azure.
Conclusion
Designing a multi‑tenant SaaS on .NET 8, EF Core, and Azure SQL Elastic Pools blends modern .NET performance with Azure’s cost‑effective scaling. By centralizing tenant resolution in middleware, applying global query filters, and leveraging elastic pool elasticity, you can support thousands of customers while keeping operational overhead predictable. The practical steps—middleware, EF Core configuration, automated database creation, and monitoring—translate directly into faster time‑to‑market and lower TCO.
Sources
Microsoft Docs – Azure SQL Elastic Pools; Microsoft Docs – EF Core Global Query Filters; Azure Architecture Center – Multi‑tenant SaaS design patterns
Author: Mahmut Sarıkaya — sarikayadev.com