DEV Community

Naimul Karim
Naimul Karim

Posted on

Simple steps to Multi-Tenant Architecture Design

Flow :

  • Request comes with InstituteId in header
  • Middleware validates and sets TenantInfo
  • TenantServiceResolver selects the service as per InstituteId in TenantInfo
  • The resolved service uses TenantDbContextFactory to select DB context
  • The rest works as per-tenant

Components:

Component Purpose
InstituteIdMiddlewar :Contains InstituteId from header
TenantInfo (Scoped) : Has the current request’s InstituteId
TenantServiceResolver :Selects services based on tenant
TenantDbContext : Per-tenant EF Core DbContext
appsettings.json : Contains list of valid tenants

1. TenantInfo Class (Contains Current Institute Id)

public class TenantInfo
{
    public string? InstituteId { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Register it as Scoped in DI:

builder.Services.AddScoped<TenantInfo>();
Enter fullscreen mode Exit fullscreen mode

2. Middleware: Extract InstituteId & Validate

public class InstituteIdMiddleware
{
    private readonly RequestDelegate _next;

    public InstituteIdMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, TenantInfo tenantInfo, IConfiguration config)
    {
        if (!context.Request.Headers.TryGetValue("InstituteId", out var instituteId))
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsync("Missing InstituteId header.");
            return;
        }

        var validTenants = config.GetSection("Tenants").Get<List<string>>();
        if (!validTenants.Contains(instituteId))
        {
            context.Response.StatusCode = 403;
            await context.Response.WriteAsync($"Unauthorized InstituteId: {instituteId}");
            return;
        }

        tenantInfo.InstituteId = instituteId;
        await _next(context);
    }
}

Enter fullscreen mode Exit fullscreen mode

Register it:

app.UseMiddleware<InstituteIdMiddleware>();
Enter fullscreen mode Exit fullscreen mode
  1. Configuration: appsettings.json
{
  "Tenants": [
    "Tenant1",
    "Tenant2"
  ],
  "ConnectionStrings": {
    "Tenant1": "Server=.;Database=Tenant1Db;Trusted_Connection=True;",
    "Tenant2": "Server=.;Database=Tenant2Db;Trusted_Connection=True;"
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Multi-Tenant DbContext

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options): base(options) { }

    public DbSet<Product> Products { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

*5. DbContext Factory *

public interface ITenantDbContextFactory
{
    ApplicationDbContext CreateDbContext();
}

public class TenantDbContextFactory : ITenantDbContextFactory
{
    private readonly IConfiguration _config;
    private readonly TenantInfo _tenantInfo;

    public TenantDbContextFactory(IConfiguration config, TenantInfo tenantInfo)
    {
        _config = config;
        _tenantInfo = tenantInfo;
    }

    public ApplicationDbContext CreateDbContext()
    {
        var instituteId = _tenantInfo.InstituteId
            ?? throw new Exception("InstituteId not set in TenantInfo");

        var connectionString = _config.GetConnectionString(instituteId);
        if (string.IsNullOrEmpty(connectionString))
            throw new Exception($"No connection string for institute: {instituteId}");

        var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
        optionsBuilder.UseSqlServer(connectionString);

        return new ApplicationDbContext(optionsBuilder.Options);
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it in DI:

builder.Services.AddScoped<ITenantDbContextFactory,TenantDbContextFactory>();
Enter fullscreen mode Exit fullscreen mode

6. Using DbContext in Service

public class Tenant1ProductService : IProductService
{
    private readonly ApplicationDbContext _db;

    public Tenant1ProductService(ITenantDbContextFactory dbContextFactory)
    {
        _db = dbContextFactory.CreateDbContext();
    }

    public string GetProductList()
    {
        var Products = _db.Products.Select(c => c.Name).ToList();
        return string.Join(", ", Products);
    }
}
Enter fullscreen mode Exit fullscreen mode

7. TenantServiceResolver

public class TenantServiceResolver : ITenantServiceResolver
{
    private readonly IServiceProvider _serviceProvider;
    private readonly TenantInfo _tenantInfo;

    public TenantServiceResolver(IServiceProvider serviceProvider, TenantInfo tenantInfo)
    {
        _serviceProvider = serviceProvider;
        _tenantInfo = tenantInfo;
    }

    public IProductService GetProductService()
    {
        return _tenantInfo.InstituteId switch
        {
            "Tenant1" => _serviceProvider.GetRequiredService<Tenant1ProductService>(),
            "Tenant2" => _serviceProvider.GetRequiredService<Tenant2ProductService>(),
            _ => throw new NotSupportedException("Invalid InstituteId")
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

*8. DI Registration *

builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<TenantInfo>();

builder.Services.AddScoped<Tenant1ProductService>();
builder.Services.AddScoped<Tenant2ProductService>();
builder.Services.AddScoped<ITenantServiceResolver, TenantServiceResolver>();

builder.Services.AddScoped<ITenantDbContextFactory, TenantDbContextFactory>();
Enter fullscreen mode Exit fullscreen mode

9. Controller

[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
    private readonly ITenantServiceResolver _tenantServiceResolver;

    public ProductController(ITenantServiceResolver tenantServiceResolver)
    {
        _tenantServiceResolver = tenantServiceResolver;
    }

    [HttpGet]
    public IActionResult GetProducts()
    {
        var productService = _tenantServiceResolver.GetProductService();
        var producs = productService.GetProductList();
        return Ok(producs);
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)