Multi-Tenancy in ASP.NET Core
Build scalable multi-tenant applications serving multiple customers.
By EMEPublished: February 20, 2025
multi-tenancysaasaspnet corearchitecturescalability
A Simple Analogy
Multi-tenancy is like apartment building management. Multiple customers (tenants) share infrastructure but each has isolated data and configuration. One bill, many customers.
Why Multi-Tenancy?
- Cost efficiency: Share infrastructure across customers
- Scalability: Serve more customers with same resources
- Revenue: SaaS pricing models
- Simplicity: Single deployment for all customers
- Customization: Per-tenant configuration
Tenant Identification
// Middleware to identify tenant
public class TenantResolutionMiddleware
{
private readonly RequestDelegate _next;
public async Task InvokeAsync(HttpContext context, ITenantService tenantService)
{
// Get tenant from subdomain
var host = context.Request.Host.Host;
var tenantId = host.Split('.')[0]; // customer.example.com -> customer
// Or from header
if (context.Request.Headers.TryGetValue("X-Tenant-ID", out var headerValue))
tenantId = headerValue.ToString();
// Or from route
if (context.GetRouteData().Values.TryGetValue("tenantId", out var routeValue))
tenantId = routeValue.ToString();
var tenant = await tenantService.GetTenantAsync(tenantId);
if (tenant == null)
return;
context.Items["Tenant"] = tenant;
await _next(context);
}
}
// Register
app.UseMiddleware<TenantResolutionMiddleware>();
Database Strategy
// Shared database with tenant column
public class Order
{
public int Id { get; set; }
public string TenantId { get; set; } // Tenant isolation
public string CustomerId { get; set; }
public decimal Total { get; set; }
}
// Database per tenant
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var tenantId = _httpContextAccessor.HttpContext.Items["TenantId"].ToString();
var connectionString = $"Server=localhost;Database=tenant_{tenantId};...";
optionsBuilder.UseSqlServer(connectionString);
}
// Hybrid: Shared catalog, separate data databases
// One central database (catalogs, tenants, users)
// One database per tenant for their data
Query Filtering by Tenant
public class OrderRepository
{
private readonly AppDbContext _context;
private readonly ITenantContext _tenantContext;
public async Task<List<Order>> GetAllAsync()
{
return await _context.Orders
.Where(o => o.TenantId == _tenantContext.TenantId) // Always filter
.ToListAsync();
}
}
// Or use interceptors
public class TenantInterceptor : SaveChangesInterceptor
{
private readonly ITenantContext _tenantContext;
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
// Automatically add TenantId to new entities
foreach (var entry in eventData.Context.ChangeTracker.Entries())
{
if (entry.Entity is ITenantEntity tenantEntity && entry.State == EntityState.Added)
{
tenantEntity.TenantId = _tenantContext.TenantId;
}
}
return new ValueTask<InterceptionResult<int>>(result);
}
}
Per-Tenant Configuration
public interface ITenant
{
string Id { get; }
string Name { get; }
Dictionary<string, string> Settings { get; }
}
public class TenantService
{
private readonly IDistributedCache _cache;
private readonly IRepository<Tenant> _repository;
public async Task<ITenant> GetTenantAsync(string tenantId)
{
var cached = await _cache.GetStringAsync($"tenant_{tenantId}");
if (!string.IsNullOrEmpty(cached))
return JsonSerializer.Deserialize<Tenant>(cached);
var tenant = await _repository.GetAsync(tenantId);
if (tenant != null)
{
var json = JsonSerializer.Serialize(tenant);
await _cache.SetStringAsync($"tenant_{tenantId}", json,
new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromHours(1)
});
}
return tenant;
}
}
Dependency Injection Per Tenant
builder.Services.AddScoped(sp =>
{
var httpContext = sp.GetRequiredService<IHttpContextAccessor>().HttpContext;
var tenant = httpContext.Items["Tenant"] as ITenant;
// Create configuration per tenant
return new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer($"Server=localhost;Database=tenant_{tenant.Id};...")
.Options);
});
Best Practices
- Prevent data leaks: Always filter by tenant
- Use interceptors: Auto-add tenant ID
- Cache tenant config: Reduce database queries
- Isolate databases: When compliance requires
- Monitor per-tenant: Track usage per customer
Related Concepts
- Row-level security (RLS)
- Tenant policies for authorization
- Usage analytics per tenant
- Custom theming per tenant
Summary
Multi-tenancy enables serving multiple customers efficiently from single infrastructure. Master data isolation, tenant identification, and per-tenant configuration to build scalable SaaS applications.