Multi-tenancy & SaaS
The RIVORA Framework provides comprehensive multi-tenancy support with flexible tenant isolation strategies, lifecycle management, and SaaS-ready features.
Tenant Resolution
The TenantMiddleware resolves the current tenant from HTTP requests using three strategies (in priority order):
- Header:
X-Tenant-Id: tenant-abc - Query string:
?tenant=tenant-abc - Subdomain:
tenant-abc.app.example.com
csharp
// In Program.cs
app.UseTenancy();ITenantStore
The ITenantStore interface validates and retrieves tenant information:
csharp
public interface ITenantStore
{
Task<TenantInfo?> GetTenantAsync(string tenantId);
}Implement this interface to back tenant resolution with your preferred storage:
csharp
public class DatabaseTenantStore : ITenantStore
{
private readonly AppDbContext _db;
private readonly IMemoryCache _cache;
public async Task<TenantInfo?> GetTenantAsync(string tenantId)
{
return await _cache.GetOrCreateAsync($"tenant:{tenantId}", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
var tenant = await _db.Tenants
.FirstOrDefaultAsync(t => t.Identifier == tenantId && t.IsActive);
if (tenant == null) return null;
return new TenantInfo
{
Id = tenant.Id.ToString(),
Name = tenant.Name,
Identifier = tenant.Identifier,
ConnectionString = tenant.ConnectionString
};
});
}
}Tenant Isolation Strategies
Strategy 1: Shared Database with Discriminator Column
All tenants share one database. Each entity has a TenantId column, and queries are automatically filtered.
csharp
builder.Services.AddRvrMultiTenancy(options =>
{
options.IsolationStrategy = TenantIsolation.SharedDatabase;
});csharp
// Automatic query filtering in the DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Automatically applied by RIVORA Framework
modelBuilder.Entity<Product>()
.HasQueryFilter(p => p.TenantId == _currentTenantId);
}Strategy 2: Database per Tenant
Each tenant gets a dedicated database. The connection string is resolved from the TenantInfo.
csharp
builder.Services.AddRvrMultiTenancy(options =>
{
options.IsolationStrategy = TenantIsolation.DatabasePerTenant;
});Strategy 3: Schema per Tenant
All tenants share one database, but each gets a separate schema.
csharp
builder.Services.AddRvrMultiTenancy(options =>
{
options.IsolationStrategy = TenantIsolation.SchemaPerTenant;
options.DefaultSchema = "shared";
});Tenant Lifecycle
Provisioning a Tenant
csharp
public class TenantManagementService
{
private readonly ITenantStore _store;
private readonly ITenantProvisioner _provisioner;
public async Task<TenantInfo> CreateTenantAsync(CreateTenantDto dto, CancellationToken ct)
{
// Validate tenant identifier uniqueness
var existing = await _store.GetTenantAsync(dto.Identifier);
if (existing != null)
throw new InvalidOperationException($"Tenant '{dto.Identifier}' already exists.");
// Create tenant record
var tenant = new TenantInfo
{
Id = Guid.NewGuid().ToString(),
Name = dto.Name,
Identifier = dto.Identifier,
IsActive = true
};
// Provision resources (database, schema, storage, etc.)
await _provisioner.ProvisionAsync(tenant, ct);
return tenant;
}
}Suspending a Tenant
csharp
public async Task SuspendTenantAsync(string tenantId, string reason, CancellationToken ct)
{
var tenant = await _store.GetTenantAsync(tenantId)
?? throw new KeyNotFoundException($"Tenant '{tenantId}' not found.");
tenant.IsActive = false;
tenant.SuspensionReason = reason;
tenant.SuspendedAt = DateTime.UtcNow;
await _store.UpdateTenantAsync(tenant, ct);
// Revoke all active sessions for this tenant
await _sessionManager.RevokeAllTenantSessionsAsync(tenantId, ct);
}Deleting a Tenant
csharp
public async Task DeleteTenantAsync(string tenantId, CancellationToken ct)
{
var tenant = await _store.GetTenantAsync(tenantId)
?? throw new KeyNotFoundException($"Tenant '{tenantId}' not found.");
// Anonymize personal data (GDPR compliance)
await _dsarService.AnonymizeTenantDataAsync(tenantId, ct);
// Deprovision resources
await _provisioner.DeprovisionAsync(tenant, ct);
// Soft-delete or hard-delete the tenant record
await _store.DeleteTenantAsync(tenantId, ct);
}Tenant Validation in Middleware
The TenantMiddleware performs two validations:
- Existence: The tenant must exist and be active in
ITenantStore - JWT match: For authenticated requests, the tenant ID from the request must match the
TenantIdclaim in the JWT
csharp
// If validation fails:
// - Unknown/inactive tenant -> 403 Forbidden
// - JWT claim mismatch -> 403 ForbiddenAccessing Current Tenant
csharp
public class ProductService
{
private readonly ITenantProvider _tenantProvider;
public async Task<List<Product>> GetProductsAsync(CancellationToken ct)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
// Query is automatically filtered by tenantId
return await _repository.GetAllAsync(ct);
}
}Registration
csharp
builder.Services.AddRvrMultiTenancy(options =>
{
options.IsolationStrategy = TenantIsolation.SharedDatabase;
options.TenantHeaderName = "X-Tenant-Id";
options.TenantQueryStringName = "tenant";
options.EnableSubdomainResolution = true;
options.CacheTenantInfo = true;
options.CacheDuration = TimeSpan.FromMinutes(5);
});
builder.Services.AddScoped<ITenantStore, DatabaseTenantStore>();
// In the pipeline
app.UseTenancy();