DEV Community

Naimul Karim
Naimul Karim

Posted on • Edited on

Simple steps to implement Multi-Tenancy Pattern

The Flow

  • Request comes with InstituteId in header
  • Middleware validates and sets InstituteId in 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

  • InstituteIdMiddleware : Extract InstituteId from header and validates it
  • TenantInfo (Scoped) : Contains 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

2. Middleware: (Extract Institute Id & 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

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

6. Using DbContext in Service

public class Tenant1ProductService : IProductService
{
    private readonly ApplicationDbContext _db;

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

    public string GetProducts()
    {
        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

public static class MultiTenantExtensions
    {
        public static void AddMultiTenantServices(this IServiceCollection services)
        {
           services.AddHttpContextAccessor();
       services.AddScoped<TenantInfo>();

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

           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.GetProducts();
        return Ok(producs);
    }
}
Enter fullscreen mode Exit fullscreen mode

10. Usage in Program.cs

var builder = WebApplication.CreateBuilder(args);

// Register custom multi-tenant services
builder.Services.AddMultiTenantServices();

// Add controllers, Swagger, etc.
builder.Services.AddControllers();

var app = builder.Build();

app.UseMiddleware<InstituteIdMiddleware>();
app.MapControllers();

app.Run();
Enter fullscreen mode Exit fullscreen mode

Top comments (0)