Multi-Tenancy
Weasel provides foundational building blocks for multi-tenant database strategies through the Weasel.Core.MultiTenancy namespace. These abstractions handle the mechanics of distributing tenants across a pool of databases and are primarily consumed by higher-level libraries like Marten and Wolverine.
Overview
In a sharded multi-tenant architecture, each tenant's data lives in one of several databases. Weasel provides the low-level plumbing for this pattern:
- A pool of databases that can accept tenants
- Assignment strategies that decide which database a new tenant goes to
- Tracking of which tenants are in which database
- CLI integration so migrations run across all tenant databases
Core Abstractions
ITenantDatabasePool
Manages the registry of databases and tenant assignments:
public interface ITenantDatabasePool_Sample
{
ValueTask<IReadOnlyList<PooledDatabase>> ListDatabasesAsync(CancellationToken ct);
ValueTask AddDatabaseAsync(string databaseId, string connectionString, CancellationToken ct);
ValueTask MarkDatabaseFullAsync(string databaseId, CancellationToken ct);
ValueTask<string?> FindDatabaseForTenantAsync(string tenantId, CancellationToken ct);
ValueTask AssignTenantAsync(string tenantId, string databaseId, CancellationToken ct);
ValueTask RemoveTenantAsync(string tenantId, CancellationToken ct);
}Each PooledDatabase tracks a database's identifier, connection string, and whether it is full (accepting no more tenants).
ITenantAssignmentStrategy
Determines which database a new tenant should be assigned to:
public interface ITenantAssignmentStrategy_Sample
{
ValueTask<string> AssignTenantToDatabaseAsync(
string tenantId,
IReadOnlyList<PooledDatabase> availableDatabases);
}Weasel ships with several built-in strategies:
| Strategy | Behavior |
|---|---|
SmallestTenantAssignment | Assigns to the database with the fewest tenants (balanced distribution). |
HashTenantAssignment | Uses a hash of the tenant ID for deterministic, even distribution. |
ExplicitTenantAssignment | Requires manual mapping of each tenant to a specific database. |
IDatabaseSizingStrategy
Controls when a database is considered "full" and should stop accepting new tenants:
public interface IDatabaseSizingStrategy_Sample
{
ValueTask<string> FindSmallestDatabaseAsync(IReadOnlyList<PooledDatabase> databases);
}IDatabase.TenantIds
The IDatabase interface includes a TenantIds property that lists which tenants are assigned to that database instance:
public interface IDatabase_TenantIds_Sample
{
List<string> TenantIds { get; }
// ... other members
}This is used by the migration infrastructure to apply schema changes to the correct databases when running in a multi-tenant configuration.
CLI Integration
Weasel's command-line tools (accessed through the JasperFx CLI) are multi-tenancy aware:
db-apply-- applies pending migrations across all tenant databases in the pool.db-assert-- verifies schema consistency across all tenant databases.--databaseflag -- filters CLI operations to a specific database by identifier, useful for targeting a single tenant database during troubleshooting.
Typical Usage
Multi-tenancy in Weasel is designed to be consumed through Marten or Wolverine rather than used directly. A typical setup through Marten looks like:
- Configure a
ITenantDatabasePoolimplementation (often backed by a master database table). - Choose an
ITenantAssignmentStrategyfor new tenant distribution. - Marten handles the rest: resolving connections per tenant, running migrations across all databases, and routing queries to the right database.
If you are building a custom multi-tenant system without Marten, you would implement ITenantDatabasePool for your storage mechanism and use ITenantAssignmentStrategy to place new tenants. The IDatabase infrastructure then ensures schema migrations are applied to every database in the pool.
