- Published on
- 🍵 4 min read
Implementing Single Database Multi-Tenancy with EF Core Global Query Filters
- Authors
- Name
- Emin Vergil
- @eminvergil
Overview
TL;DR
This blog post shows how to set up multi-tenancy in a single database using Entity Framework Core. It explains how to create a TenantProvider, configure the DbContext, set up middleware for tenant ID extraction
Introduction
Multi-tenancy is a design pattern where a single instance of an application serves multiple tenants. This blog post will demonstrate how to implement single database multi-tenancy using Entity Framework Core with global query filters
Example
1 - Define the Tenant Provider
Create a TenantProvider
to hold the current tenant’s ID
public class TenantProvider
{
public int TenantId { get; set; }
}
2 - Configure the DbContext
Add the global query filter to your DbContext
to ensure tenant-specific data is automatically filtered
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
private readonly TenantProvider _tenantProvider;
public AppDbContext(DbContextOptions<AppDbContext> options, TenantProvider tenantProvider)
: base(options)
{
_tenantProvider = tenantProvider;
}
public virtual DbSet<CatalogEntity> Catalogs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CatalogEntity>()
.HasQueryFilter(y => y.TenantId == _tenantProvider.TenantId);
}
}
3 - Middleware for Tenant
Create middleware to extract the x-tenant-id
header and set it in the TenantProvider
public class TenantMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context, TenantProvider tenantProvider)
{
if (context.Request.Headers.TryGetValue("x-tenant-id", out var tenantIdStr) &&
int.TryParse(tenantIdStr, out var tenantId))
{
tenantProvider.TenantId = tenantId;
}
else
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync("Tenant ID is missing.");
return;
}
await next(context);
}
}
4 - Register Services
builder.Services.AddSingleton<TenantProvider>();
builder.Services.AddDbContext<AppDbContext>(
options => options.UseInMemoryDatabase("catalog-api")
);
....
app.UseMiddleware<TenantMiddleware>();
5 - Create Endpoints
app.MapPost("/create", async ([FromBody] CreateCatalogRequestModel request, AppDbContext dbContext, TenantProvider tenantProvider) =>
{
var catalogEntity = new CatalogEntity
{
Name = request.Name,
TenantId = tenantProvider.TenantId
};
await dbContext.Catalogs.AddAsync(catalogEntity);
await dbContext.SaveChangesAsync();
return Results.Ok();
});
app.MapGet("/get", async (AppDbContext dbContext) =>
{
var catalogEntities = await dbContext.Catalogs.ToListAsync();
return Results.Ok(catalogEntities);
});
Test
Here are example cURL requests:
curl -X POST -H "x-tenant-id: 1" -H "Content-Type: application/json" \
-d '{"name": "Sample for tenant id 1"}' \
http://localhost:3000/create
curl -H "x-tenant-id: 1" http://localhost:3000/get
As you can see, we get the records created with x-tenant-id: 1
Now, let's create another record with the x-tenant-id: 2
header:
curl -X POST -H "x-tenant-id: 2" -H "Content-Type: application/json" \
-d '{"name": "Sample for tenant id 2"}' \
http://localhost:3000/create
curl -H "x-tenant-id: 2" http://localhost:3000/get
As you can see, only the records from x-tenant-id: 2
are returned because of the global query filter.
Conclusion
In this post, we covered implementing multi-tenancy with Entity Framework Core global query filters. While we used headers here, the tenant ID can also come from tokens or query parameters, depending on your needs.
For code examples, check out the project here: GitHub - catalog-api.