Multi-tenancy is a powerful architectural pattern where a single application instance serves multiple customers (tenants), each logically isolated from the other. This approach is widely adopted in SaaS (Software-as-a-Service) solutions for efficiency, scalability, and cost optimization.
In this article, we will explore how to design and implement multi-tenant architecture in ASP.NET Core using Entity Framework Core (EF Core) focusing on strategies for tenant identification, database isolation, and data management.
1. Understanding Multi-Tenancy Models
Before jumping into implementation, it’s important to understand the common types of multi-tenancy:
a) Single Database, Shared Schema
All tenants share the same database and tables. Tenant data is distinguished by a TenantId column in each table.
Pros: Simple to maintain, cost-effective.
Cons: Limited data isolation, higher risk of cross-tenant data leaks.
b) Single Database, Separate Schemas
Each tenant has its own schema (like TenantA.Users, TenantB.Users).
c) Separate Databases per Tenant
Each tenant has its own dedicated database.
2. Setting Up Tenant Identification Middleware
Every request must identify which tenant it belongs to. Common identification strategies include:
Subdomain-based (e.g., tenant1.app.com)
Path-based (e.g., app.com/tenant1)
Header-based (custom header like X-Tenant-ID)
Example: Tenant Identification Middleware
public class TenantMiddleware
{
private readonly RequestDelegate _next;
public TenantMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context, ITenantService tenantService)
{
var tenantId = context.Request.Headers["X-Tenant-ID"].FirstOrDefault();
if (string.IsNullOrEmpty(tenantId))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Tenant ID missing");
return;
}
tenantService.SetTenant(tenantId);
await _next(context);
}
}
And register it in Startup.cs or Program.cs:
app.UseMiddleware<TenantMiddleware>();
3. Creating the Tenant Service
This service manages the active tenant for the current request.
public interface ITenantService
{
string GetTenant();
void SetTenant(string tenantId);
}
public class TenantService : ITenantService
{
private string _tenantId;
public string GetTenant() => _tenantId;
public void SetTenant(string tenantId) => _tenantId = tenantId;
}
Register it as a scoped service:
builder.Services.AddScoped<ITenantService, TenantService>();
4. Configuring EF Core for Multi-Tenancy
If using separate databases, dynamically build the connection string per tenant:
public class TenantDbContext : DbContext
{
private readonly ITenantService _tenantService;
private readonly IConfiguration _configuration;
public TenantDbContext(DbContextOptions<TenantDbContext> options, ITenantService tenantService, IConfiguration configuration)
: base(options)
{
_tenantService = tenantService;
_configuration = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var tenantId = _tenantService.GetTenant();
var connectionString = _configuration.GetConnectionString("DefaultConnection").Replace("{tenantId}", tenantId);
optionsBuilder.UseSqlServer(connectionString);
}
public DbSet<Customer> Customers { get; set; }
}
In your appsettings.json:
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=Tenant_{tenantId};Trusted_Connection=True;"
}
5. Applying a Tenant Filter (Shared Schema Approach)
If using a shared schema, apply a global query filter:
public class ApplicationDbContext : DbContext
{
private readonly ITenantService _tenantService;
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, ITenantService tenantService)
: base(options)
{
_tenantService = tenantService;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>().HasQueryFilter(c => c.TenantId == _tenantService.GetTenant());
}
public DbSet<Customer> Customers { get; set; }
}
This ensures that all queries automatically filter by the current tenant’s data.
6. Example Controller
[ApiController]
[Route("api/[controller]")]
public class CustomersController : ControllerBase
{
private readonly TenantDbContext _context;
public CustomersController(TenantDbContext context)
{
_context = context;
}
[HttpGet]
public async Task<IActionResult> GetCustomers()
{
var customers = await _context.Customers.ToListAsync();
return Ok(customers);
}
}
7. Practical Example in Action
Request
GET /api/customers
Header: X-Tenant-ID = TenantA
Database Connection Used
Server=.;Database=Tenant_TenantA;Trusted_Connection=True;
Result
Returns only data from TenantA’s database or filtered by TenantA’s TenantId.
8. Best Practices
Use caching for tenant configuration lookups.
Secure tenant isolation — never allow direct TenantId exposure in client-side apps.
Automate database provisioning using background jobs.
Use migrations per tenant if using isolated databases.
Regularly audit access and data isolation between tenants.
Conclusion
Implementing multi-tenancy in ASP.NET Core with EF Core enables scalability, cost efficiency, and easier maintenance for SaaS products. Depending on your business requirements whether lightweight shared databases or fully isolated ones — you can tailor this architecture to ensure each tenant’s data is secure, consistent, and high-performing.
A well-designed multi-tenant system not only reduces infrastructure costs but also provides a strong foundation for expanding your application to hundreds of tenants with minimal architectural change.