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

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 !
The database schema in code first
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
The Client class
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; }
}
The MultitenantContext class (using SQL Server)
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();
});
}
}
}
The API project
Next is the API project to request data




The ClientsController class
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);
}
}
}
The ClientResource model
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; }
}
}
The appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Default": "Data Source=.;Initial Catalog=Multitenant;Integrated Security=SSPI;Persist Security Info=False;TrustServerCertificate=false;"
}
}
The multi-tenant scenario
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
The multi-tenant implementation
The interface
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; }
}
}
The instance
Now, the implementation
namespace Multitenant;
/// <summary>
/// The tenant implementation
/// </summary>
public class Tenant : ITenant
{
/// <inheritdoc/>
public Guid TenantId { get; set; }
}
The dependency injection
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();
}
}
}
The connection string prefix or suffix
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;"
}
}
The database context
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.
The migrations
To generate migrations, we need to use a special nuget package dedicated to this
- Microsoft.EntityFrameworkCore.Design in version 9.0.9
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.

The POSTMAN collection
There are multi ways to do the test, I will illustrate one of them with POSTMAN but you can choose what you prefer.
- Create a new collection by clicking on the New button and choose in the popup Collection

- Click on Add a request

- Set the URI and keep the verb on GET. The host can be determined in the Properties/launchSettings.json. Search in profiles, the https. Take the first part of applicationUrl and add /clients
For me, I will have https://localhost:7208/clients
Add a header
X-tenant-idwith the GUIDb9af52c6-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


Conclusion
Thanks for reading. Now you know how to create a multi-tenant system with one database per tenant. Enjoy !
Let's Connect!
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






