How to generate a database per tenant in multi-tenant system ?

Search for a command to run...

Maybe you already encounter this problem. You want to use the IHttpContextAccessor but it is not available and there is no indication to find the right package to use. To solve this situation, you just have to use the Microsoft.AspNetCore.Http.Abstra...

I previously explain how to create a custom configuration section in a web.config. But something miss if we want to do all the job. If you share the library with your favorites colleagues and the haven't the documentation, it is better to include the...

Here's a topic that's really interesting for many developers who like to organize their code a bit. Having a custom configuration section in a configuration file is the best approach when that configuration involves a business need, a connection to a...

There is often the question from my colleagues about how to do a quick paged requests on records without typing a lot of SQL code in stored procedure or in code by filtering a collection of records. The answer for this question comes with EF Core but...

The purpose is to be able to create a multi-tenant database system where each tenant have its own database. We will see the different steps to implement this configuration
Let’s go, begin with the STARTER KIT !
First of all, we will create the database context. No matter what we put inside, it is just to describe the project.



Import Microsoft.EntityFrameworkCore, Microsoft.EntityFrameworkCore.Tools in version 9.0.9
namespace Multitenant;
/// <summary>
/// The client entity
/// </summary>
public class Client
{
/// <summary>
/// The identifier
/// </summary>
public int Id { get; set; }
/// <summary>
/// The name
/// </summary>
public string Name { get; set; }
/// <summary>
/// The birthdate
/// </summary>
public DateTime BirthDate { get; set; }
}
You have to import :
Microsoft.EntityFrameworkCore.SqlServer in version 9.0.9
Microsoft.Extensions.Configuration.Abstractions in version 9.0.9
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace Multitenant
{
public class MultitenantContext : DbContext
{
/// <summary>
/// The clients DbSet
/// </summary>
public DbSet<Client> Clients { get; set; }
private readonly IConfiguration _configuration;
/// <summary>
/// Constructor
/// </summary>
/// <param name="options">The database context options instance</param>
/// <param name="configuration">The configuration instance</param>
public MultitenantContext(DbContextOptions<MultitenantContext> options, IConfiguration configuration)
: base(options)
{
_configuration = configuration;
}
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseSqlServer(_configuration.GetConnectionString("Default"));
}
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Client>(b =>
{
b.HasKey(x => x.Id);
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
b.Property(x => x.BirthDate).IsRequired();
});
}
}
}
Next is the API project to request data




Remove the WeatherForecastController class and WeatherForecast model
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Multitenant.Api.Models;
namespace Multitenant.Api.Controllers
{
[ApiController]
public class ClientsController : ControllerBase
{
private readonly MultitenantContext _context;
private readonly ILogger<ClientsController> _logger;
/// <summary>
/// Constructor
/// </summary>
/// <param name="context">The database context</param>
/// <param name="logger">The logger instance</param>
public ClientsController(MultitenantContext context, ILogger<ClientsController> logger)
{
_context = context;
_logger = logger;
}
[HttpGet("clients")]
[ProducesResponseType(typeof(IEnumerable<ClientResource>), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> GetAll()
{
await _context.Database.MigrateAsync();
var response = await _context.Clients.ToListAsync();
var result = response.ConvertAll(x => new ClientResource { Name = x.Name, BirthDate = x.BirthDate });
if(result.Count > 0)
return Ok(result);
return NoContent();
}
[HttpGet("clients/{id:int}")]
[ProducesResponseType(typeof(ClientResource), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> GetById(int id)
{
await _context.Database.MigrateAsync();
var client = await _context.Clients.FindAsync(id);
if (client == null)
{
return NotFound();
}
var result = new ClientResource { Name = client.Name, BirthDate = client.BirthDate };
return Ok(result);
}
}
}
Create a folder Models in the project
namespace Multitenant.Api.Models
{
/// <summary>
/// The client resource
/// </summary>
public class ClientResource
{
/// <summary>
/// The name
/// </summary>
public string Name { get; set; }
/// <summary>
/// The birthdate
/// </summary>
public DateTime BirthDate { get; set; }
}
}
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Default": "Data Source=.;Initial Catalog=Multitenant;Integrated Security=SSPI;Persist Security Info=False;TrustServerCertificate=false;"
}
}
To handle the multi-tenant, try to understand the scenario. On each request, we want to identify the tenant calling the API and request its database to restitute their data.
So, to answer to this scenario, we have to do these steps:
Be able to identify the tenant
Use the tenant to configure the DbContext connection string to target the right database
First of all, we have to define the tenant interface
namespace Multitenant
{
/// <summary>
/// The tenant interface
/// </summary>
public interface ITenant
{
/// <summary>
/// The tenant identifier
/// </summary>
public Guid TenantId { get; set; }
}
}
Now, the implementation
namespace Multitenant;
/// <summary>
/// The tenant implementation
/// </summary>
public class Tenant : ITenant
{
/// <inheritdoc/>
public Guid TenantId { get; set; }
}
Next is the dependency injection, add scoped the ITenant implementation.
using Microsoft.EntityFrameworkCore;
namespace Multitenant.Api
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MultitenantContext>();
//NOTICE: Configure the ITenant service to be resolved from the X-Tenant-Id header
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenant>(sp =>
{
var tenantIdString = sp.GetRequiredService<IHttpContextAccessor>().HttpContext?.Request.Headers["X-Tenant-Id"];
return (!string.IsNullOrEmpty(tenantIdString) && Guid.TryParse(tenantIdString, out var tenantId) ? new Tenant { TenantId = tenantId } : null)!;
});
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
To be able to address a database per tenant, we have to change the database name in the connection string according to the tenant. To do it, we will pattern the connection string in the configuration file.
I choose SUFFIX but you can change if you want to use PREFIX for sure
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Default": "Data Source=.;Initial Catalog=Multitenant__Suffix__;Integrated Security=SSPI;Persist Security Info=False;TrustServerCertificate=false;"
}
}
Now we have defined the bases, we have to change the database context to mutate per tenant. To do it, we will add the ITenant interface to change the connection string dynamically.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace Multitenant
{
public class MultitenantContext : DbContext
{
/// <summary>
/// The clients DbSet
/// </summary>
public DbSet<Client> Clients { get; set; }
private readonly IConfiguration _configuration;
private readonly ITenant _tenant;
/// <summary>
/// Constructor
/// </summary>
/// <param name="options">The database context options instance</param>
/// <param name="tenant">The tenant instance</param>
/// <param name="configuration">The configuration instance</param>
public MultitenantContext(DbContextOptions<MultitenantContext> options, ITenant tenant, IConfiguration configuration)
: base(options)
{
_tenant = tenant;
_configuration = configuration;
}
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
var connectionString = _configuration.GetConnectionString("Default")?.Replace("__Suffix__", _tenant?.TenantId.ToString() ?? "default");
options.UseSqlServer(connectionString);
}
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Client>(b =>
{
b.HasKey(x => x.Id);
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
b.Property(x => x.BirthDate).IsRequired();
});
}
}
}
The default fallback value is used during the EF design-time and let you generate the default database and generate migrations.
To generate migrations, we need to use a special nuget package dedicated to this
Import this package in the API project and set it at Startup project
Now, open Package Manager Console, and type :
Add-Migration Init -context MultitenantContext
You will have a new folder Migrations containing the scripts to migrate the database.

There are multi ways to do the test, I will illustrate one of them with POSTMAN but you can choose what you prefer.


For me, I will have https://localhost:7208/clients
Add a header X-tenant-id with the GUID b9af52c6-0edd-4301-91fe-08dd10851075

Launch the API project

Launch the query in Postman by clicking on Send button. Show the result 204 No Content and a new database created


Thanks for reading. Now you know how to create a multi-tenant system with one database per tenant. Enjoy !
Hi, I'm Yoann. I work as a full-stack developer, solution architect.
If you enjoyed this article, you might enjoy my other content, too.
Github: yblossier
LinkedIn: /in/yoannblossier
Buy Me A Coffee: A special thank you for your support 🍵
Thank you for joining me today.
Yoann