Skip to main content

Command Palette

Search for a command to run...

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

Updated
7 min read
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 await _context.Database.MigrateAsync() is here to ensure the database will be created and migrated. In real implementation, this call should be done in the Store implementation and not here !

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:

  1. Be able to identify the tenant

  2. 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.

If you launch the command Update-Database you will generate the database Multitenantdefault. The default value comes from onConfiguring where we configure the fallback value in case the tenant value is null.

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.

  1. Create a new collection by clicking on the New button and choose in the popup Collection

  1. Click on Add a request

  1. 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

  1. Add a header X-tenant-id with the GUID b9af52c6-0edd-4301-91fe-08dd10851075

  2. 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