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; }
}
Register it as Scoped in DI:
builder.Services.AddScoped<TenantInfo>();
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);
}
}
Register it:
app.UseMiddleware<InstituteIdMiddleware>();
- 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);
}
}
Register it in DI:
builder.Services.AddScoped<ITenantDbContextFactory,TenantDbContextFactory>();
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);
}
}
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 *
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>();
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);
}
}
Top comments (0)