Multi-Tenant Applications with EF Core
This page is a summary of Multi-Tenant Applications With EF Core by Milan Jovanović.
Most modern software applications are built around the concept of multi-tenancy — one application serves multiple customers while keeping their data isolated.
Multi-Tenancy Strategies
There are two main approaches to multi-tenancy:
| Strategy | Description |
|---|---|
| Single Database | Share a single DB and separate data logically using a TenantId column |
| Multiple Databases | Use a separate DB per tenant for physical data isolation |
The choice depends on your requirements. Some industries like healthcare require a high degree of data isolation, making a database-per-tenant mandatory.
EF Core Query Filters Primer
EF Core's Global Query Filters automatically apply a filter to all queries for a given entity.
Basic Usage
modelBuilder
.Entity<Order>()
.HasQueryFilter(order => !order.IsDeleted);
- Configure the filter by calling
HasQueryFilteron the entity - EF Core applies it to all queries for that entity automatically
- Disable for a specific query with
IgnoreQueryFilters() - Only one query filter per entity is allowed
With the example above, every query against the Order table will include an IsDeleted = FALSE condition.
Single-Database Multi-Tenancy with EF Core
Two things are needed to implement multi-tenancy on a single database:
- A way to identify the current tenant
- A filter that limits results to that tenant's data
The standard approach is to add a TenantId column to each table and filter on it during queries.
DbContext Implementation
public class OrdersDbContext : DbContext
{
private readonly string _tenantId;
public OrdersDbContext(
DbContextOptions<OrdersDbContext> options,
TenantProvider tenantProvider)
: base(options)
{
_tenantId = tenantProvider.TenantId;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Order>()
.HasQueryFilter(o => o.TenantId == _tenantId);
}
}
The current tenant value is obtained from TenantProvider and wired up as a Query Filter.
TenantProvider Implementation
public sealed class TenantProvider
{
private const string TenantIdHeaderName = "X-TenantId";
private readonly IHttpContextAccessor _httpContextAccessor;
public TenantProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string TenantId => _httpContextAccessor
.HttpContext
.Request
.Headers[TenantIdHeaderName];
}
In this example, TenantId is read from the HTTP request header X-TenantId.
Ways to Provide TenantId
| Method | Example | Security |
|---|---|---|
| HTTP Header | X-TenantId: tenant-1 | Low |
| Query String | api/orders?tenantId=tenant-1 | Low |
| JWT Claim | "tenant_id": "tenant-1" | High |
| API Key | Authorization: ApiKey ... | High |
For a more secure implementation, prefer JWT Claims or API Keys.
Multi-Database Multi-Tenancy with EF Core
To isolate each tenant to a separate database, two changes are required:
- Apply a different connection string per tenant
- Resolve the correct connection string for the current tenant
Query Filters are not applicable here since each tenant uses a different database.
Tenant information and connection strings must be stored and resolved separately.
appsettings.json Example
{
"Tenants": [
{ "Id": "tenant-1", "ConnectionString": "Host=tenant1.db;Database=tenant1" },
{ "Id": "tenant-2", "ConnectionString": "Host=tenant2.db;Database=tenant2" }
]
}
Register an IOptions instance containing a list of Tenant objects.
TenantProvider (Multi-Database Version)
public sealed class TenantProvider
{
private const string TenantIdHeaderName = "X-TenantId";
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly TenantSettings _tenantSettings;
public TenantProvider(
IHttpContextAccessor httpContextAccessor,
IOptions<TenantSettings> tenantsOptions)
{
_httpContextAccessor = httpContextAccessor;
_tenantSettings = tenantsOptions.Value;
}
public string TenantId => _httpContextAccessor
.HttpContext
.Request
.Headers[TenantIdHeaderName];
public string GetConnectionString()
{
return _tenantSettings.Tenants
.Single(t => t.Id == TenantId)
.ConnectionString;
}
}
Dynamically Registering DbContext
builder.Services.AddDbContext<OrdersDbContext>((sp, options) =>
{
var tenantProvider = sp.GetRequiredService<TenantProvider>();
var connectionString = tenantProvider.GetConnectionString();
options.UseSqlServer(connectionString);
});
On every request a new OrdersDbContext is created that connects to the appropriate database for the current tenant.
Store tenant connection strings in a secure location such as Azure Key Vault rather than plain configuration files.
Strategy Comparison
| Item | Single Database | Multiple Databases |
|---|---|---|
| Data Isolation | Logical (TenantId column) | Physical (separate DBs) |
| Cost | Low | High |
| Scalability | DB load concentrates as tenants increase | Each tenant can scale independently |
| Operational Complexity | Low | High (migrations become complex) |
| Regulatory Compliance | May be insufficient for strict requirements | Suitable for healthcare, finance, etc. |
| Query Filter | Applied automatically via HasQueryFilter | Not needed (DBs are already isolated) |
| Connection String Management | Single | One per tenant |
Multi-Tenancy in Background Jobs
Background jobs cannot access HttpContext, so special care is needed to obtain the TenantId.
- Include
TenantIdin job data when scheduling the job - If a User ID is available in the job, look up
TenantIdfrom the database using it - When using message queues, include
TenantIdin the message payload
Summary
Key points for implementing multi-tenancy with EF Core:
- Single DB: Use Global Query Filters combined with a
TenantIdcolumn - Multiple DBs: Dynamically switch connection strings per tenant
- Use
TenantProviderto identify the current tenant on every request - Prefer JWT Claims or API Keys for secure
TenantIdpropagation - Store connection strings in a secure vault such as Azure Key Vault
Once you understand the basic principles, you can evolve this foundation into a more robust and secure multi-tenant system.