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; } }
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); } }
3. Configuration: appsettings.json
{ "Tenants": [ "Tenant1", "Tenant2" ], "ConnectionStrings": { "Tenant1": "Server=.;Database=Tenant1Db;Trusted_Connection=True;", "Tenant2": "Server=.;Database=Tenant2Db;Trusted_Connection=True;" } }
4. Multi-Tenant DbContext
public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options): base(options) { } public DbSet<Product> Products { get; set; } }
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); } }
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); } }
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") }; } }
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>(); } }
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); } }
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();
Top comments (0)