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)