Skip to content

Use this LLM Friendly Docs as an MCP server for Marten.

The search box in the website knows all the secrets—try it!

For any queries, join our Discord Channel to reach us faster.

JasperFx Logo

JasperFx provides formal support for Marten and other JasperFx libraries. Please check our Support Plans for more details.

Multi-Tenancy with Database per Tenant

Marten has support for two types of multi-tenanted storage and data retrieval, conjoined multi-tenancy where data for separate tenants is stored in the same tables, but separated by a tenant_id column. Marten can also efficiently separate tenant data by using separate databases for each tenant or for a group of logical tenants.

First off, let's try to answer the obvious questions you probably have:

  • Can I combine conjoined multi-tenancy and database per tenant? - That's a yes.
  • Does Marten know how to handle database migrations with multiple databases? - Yes, and that was honestly most of the work to support this functionality:(
  • Will the async daemon work with multiple databases? - Yes, and there's nothing else you need to do to enable that on the async daemon side
  • What strategies does Marten support out of the box for this? - That's explained in the next two sections below.
  • If Marten doesn't do what I need for this feature, can I plug in my own strategy? - That's also a yes, see the section on writing your own.
  • Does the IDocumentStore.Advanced features work for multiple databases? - This is a little more complicated, but the answer is still yes. See the very last section on administering databases.
  • Can this strategy use different database schemas in the same database? - That's a hard no. The databases have to be identical in all structures.

Tenant Id Case Sensitivity

Hey, we've all been there. Our perfectly crafted code fails because of a @#$%#@%ing case sensitivity string comparison. That's unfortunately happened to Marten users with the tenantId values passed into Marten, and it's likely to happen again. To guard against that, you can force Marten to convert all supplied tenant ids from the outside world to either upper or lower case to try to stop these kinds of case sensitivity bugs in their tracks like so:

cs
var store = DocumentStore.For(opts =>
{
    // This is the default
    opts.TenantIdStyle = TenantIdStyle.CaseSensitive;

    // Or opt into this behavior:
    opts.TenantIdStyle = TenantIdStyle.ForceLowerCase;

    // Or force all tenant ids to be converted to upper case internally
    opts.TenantIdStyle = TenantIdStyle.ForceUpperCase;
});

snippet source | anchor

Static Database to Tenant Mapping

INFO

This is a simple option for a static number of tenant databases that may or may not be housed in the same physical PostgreSQL instance. Marten does not automatically create the databases themselves.

The first and simplest option built in is the MultiTenantedDatabases() syntax that assumes that all tenant databases are built upfront and there is no automatic database provisioning at runtime. In this case, you can supply the mapping of databases to tenant id as shown in the following code sample:

cs
_host = await Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        services.AddMarten(opts =>
            {
                // Explicitly map tenant ids to database connection strings
                opts.MultiTenantedDatabases(x =>
                {
                    // Map multiple tenant ids to a single named database
                    x.AddMultipleTenantDatabase(db1ConnectionString, "database1")
                        .ForTenants("tenant1", "tenant2");

                    // Map a single tenant id to a database, which uses the tenant id as well for the database identifier
                    x.AddSingleTenantDatabase(tenant3ConnectionString, "tenant3");
                    x.AddSingleTenantDatabase(tenant4ConnectionString, "tenant4");
                });

                opts.RegisterDocumentType<User>();
                opts.RegisterDocumentType<Target>();
            })

            // All detected changes will be applied to all
            // the configured tenant databases on startup
            .ApplyAllDatabaseChangesOnStartup();
    }).StartAsync();

snippet source | anchor

Single Instance Multi-Tenancy

INFO

This might be the simplest possible way to get started with multi-tenancy per database. In only this case, Marten is able to build any missing tenant databases based on the tenant id.

The second out of the box option is to use a separate named database in the same database instance for each individual tenant. In this case, Marten is able to provision new tenant databases on the fly when a new tenant id is encountered for the first time. That will obviously depend on the application having sufficient permissions for this to work. We think this option may be mostly suitable for development and automated testing rather than production usage.

This usage is shown below:

cs
_host = await Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        services.AddMarten(opts =>
        {
            opts
                // You have to specify a connection string for "administration"
                // with rights to provision new databases on the fly
                .MultiTenantedWithSingleServer(
                    ConnectionSource.ConnectionString,
                    t => t
                        // You can map multiple tenant ids to a single named database
                        .WithTenants("tenant1", "tenant2").InDatabaseNamed("database1")

                        // Just declaring that there are additional tenant ids that should
                        // have their own database
                        .WithTenants("tenant3", "tenant4") // own database
                );

            opts.RegisterDocumentType<User>();
            opts.RegisterDocumentType<Target>();
        }).ApplyAllDatabaseChangesOnStartup();
    }).StartAsync();

snippet source | anchor

Master Table Tenancy Model

INFO

Use this option if you have any need to add new tenant databases at runtime without incurring any application downtime. This option may also be easier to maintain than the static mapped option if the number of tenant databases grows large, but that's going to be a matter of preference and taste rather than any hard technical reasoning

New in Marten 7.0 is a built in recipe for database multi-tenancy that allows for new tenant database to be discovered at runtime using this syntax option:

cs
using var host = await Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        services.AddMarten(sp =>
            {
                var configuration = sp.GetRequiredService<IConfiguration>();
                var masterConnection = configuration.GetConnectionString("master");
                var options = new StoreOptions();

                // This is opting into a multi-tenancy model where a database table in the
                // master database holds information about all the possible tenants and their database connection
                // strings
                options.MultiTenantedDatabasesWithMasterDatabaseTable(x =>
                {
                    x.ConnectionString = masterConnection;

                    // You can optionally configure the schema name for where the mt_tenants
                    // table is stored
                    x.SchemaName = "tenants";

                    // If set, this will override the database schema rules for
                    // only the master tenant table from the parent StoreOptions
                    x.AutoCreate = AutoCreate.CreateOrUpdate;

                    // Optionally seed rows in the master table. This may be very helpful for
                    // testing or local development scenarios
                    // This operation is an "upsert" upon application startup
                    x.RegisterDatabase("tenant1", configuration.GetConnectionString("tenant1"));
                    x.RegisterDatabase("tenant2", configuration.GetConnectionString("tenant2"));
                    x.RegisterDatabase("tenant3", configuration.GetConnectionString("tenant3"));

                    // Tags the application name to all the used connection strings as a diagnostic
                    // Default is the name of the entry assembly for the application or "Marten" if
                    // .NET cannot determine the entry assembly for some reason
                    x.ApplicationName = "MyApplication";
                });

                // Other Marten configuration

                return options;
            })
            // All detected changes will be applied to all
            // the configured tenant databases on startup
            .ApplyAllDatabaseChangesOnStartup();;
    }).StartAsync();

snippet source | anchor

With this model, Marten is setting up a table named mt_tenant_databases to store with just two columns:

  1. tenant_id
  2. connection_string

At runtime, when you ask for a new session for a specific tenant like so:

csharp
using var session = store.LightweightSession("tenant1");

This new Marten tenancy strategy will first look for a database with the “tenant1” identifier its own memory, and if it’s not found, will try to reach into the database table to “find” the connection string for this newly discovered tenant. If a record is found, the new tenancy strategy caches the information, and proceeds just like normal.

Now, let me try to anticipate a couple questions you might have here:

  • Can Marten track and apply database schema changes to new tenant databases at runtime? Yes, Marten does the schema check tracking on a database by database basis. This means that if you add a new tenant database to that underlying table, Marten will absolutely be able to make schema changes as needed to just that tenant database regardless of the state of other tenant databases.
  • Will the Marten command line tools recognize new tenant databases? Yes, same thing. If you call dotnet run -- marten-apply for example, Marten will do the schema migrations independently for each tenant database, so any outstanding changes will be performed on each tenant database.
  • Can Marten spin up asynchronous projections for a new tenant database without requiring downtime? Yes! Check out this big ol’ integration test proving that the new Marten V7 version of the async daemon can handle that just fine:
csharp
[Fact]
public async Task add_tenant_database_and_verify_the_daemon_projections_are_running()
{
    // In this code block, I'm adding new tenant databases to the system that I
    // would expect Marten to discover and start up an asynchronous projection
    // daemon for all three newly discovered databases
    var tenancy = (MasterTableTenancy)theStore.Options.Tenancy;
    await tenancy.AddDatabaseRecordAsync("tenant1", tenant1ConnectionString);
    await tenancy.AddDatabaseRecordAsync("tenant2", tenant2ConnectionString);
    await tenancy.AddDatabaseRecordAsync("tenant3", tenant3ConnectionString);
 
    // This is a new service in Marten specifically to help you interrogate or
    // manipulate the state of running asynchronous projections within the current process
    var coordinator = _host.Services.GetRequiredService<IProjectionCoordinator>();
    var daemon1 = await coordinator.DaemonForDatabase("tenant1");
    var daemon2 = await coordinator.DaemonForDatabase("tenant2");
    var daemon3 = await coordinator.DaemonForDatabase("tenant3");
 
    // Just proving that the configured projections for the 3 new databases
    // are indeed spun up and running after Marten's new daemon coordinator
    // "finds" the new databases
    await daemon1.WaitForShardToBeRunning("TripCustomName:All", 30.Seconds());
    await daemon2.WaitForShardToBeRunning("TripCustomName:All", 30.Seconds());
    await daemon3.WaitForShardToBeRunning("TripCustomName:All", 30.Seconds());
}

At runtime, if the Marten V7 version of the async daemon (our sub system for building asynchronous projections constantly in a background IHostedService) is constantly doing “health checks” to make sure that some process is running all known asynchronous projections on all known client databases. Long story, short, Marten 7 is able to detect new tenant databases and spin up the asynchronous projection handling for these new tenants with zero downtime.

Sharded Multi-Tenancy with Database Pooling 8.x

TIP

This strategy was designed for extreme scalability scenarios targeting hundreds of billions of events across many tenants. It combines database-level sharding, conjoined tenancy, and native PostgreSQL list partitioning into a single cohesive multi-tenancy model.

For systems with very large numbers of tenants and massive data volumes, Marten provides a sharded tenancy model that distributes tenants across a pool of databases. Within each database, tenant data is physically isolated using native PostgreSQL LIST partitioning by tenant ID, while Marten's conjoined tenancy handles the query-level filtering.

This approach gives you:

  • Horizontal scaling — spread data across many databases to distribute I/O and storage
  • Physical tenant isolation — each tenant has its own PostgreSQL partitions for both document and event tables
  • Dynamic tenant routing — new tenants are automatically assigned to databases based on a pluggable strategy
  • Runtime expandability — add new databases to the pool without downtime

Configuration

csharp
var builder = Host.CreateApplicationBuilder();
builder.Services.AddMarten(opts =>
{
    opts.MultiTenantedWithShardedDatabases(x =>
    {
        // Connection to the master database that holds the pool registry
        x.ConnectionString = masterConnectionString;

        // Schema for the registry tables in the master database
        x.SchemaName = "tenants";

        // Schema for the partition tracking table within each tenant database
        x.PartitionSchemaName = "partitions";

        // Seed the database pool on startup
        x.AddDatabase("shard_01", shard1ConnectionString);
        x.AddDatabase("shard_02", shard2ConnectionString);
        x.AddDatabase("shard_03", shard3ConnectionString);
        x.AddDatabase("shard_04", shard4ConnectionString);

        // Choose a tenant assignment strategy (see below)
        x.UseHashAssignment(); // this is the default
    });
});

Calling MultiTenantedWithShardedDatabases() automatically enables:

  • Policies.AllDocumentsAreMultiTenanted() — all document types use conjoined tenancy
  • Events.TenancyStyle = TenancyStyle.Conjoined — events are partitioned by tenant
  • Policies.PartitionMultiTenantedDocumentsUsingMartenManagement() — native PG list partitions are created per tenant

Tenant Assignment Strategies

When a previously unknown tenant ID is encountered, Marten needs to decide which database in the pool should host that tenant. Three built-in strategies are available, and you can provide your own.

Hash Assignment (Default)

csharp
x.UseHashAssignment();

Uses a deterministic FNV-1a hash of the tenant ID modulo the number of available (non-full) databases. This is the fastest strategy and requires no database queries to make the assignment decision. The same tenant ID will always hash to the same database, making it predictable and debuggable.

Best for: systems where tenants are roughly equal in size and you want even distribution without any management overhead.

Smallest Database Assignment

csharp
x.UseSmallestDatabaseAssignment();

// Or with a custom sizing strategy
x.UseSmallestDatabaseAssignment(new MyCustomSizingStrategy());

Assigns new tenants to the database with the fewest existing tenants. By default, "smallest" is determined by the tenant_count column in the pool registry table. You can provide a custom IDatabaseSizingStrategy implementation that queries actual row counts, disk usage, or any other metric to determine database capacity.

csharp
public interface IDatabaseSizingStrategy
{
    ValueTask<string> FindSmallestDatabaseAsync(
        IReadOnlyList<PooledDatabase> databases);
}

Best for: systems where tenants vary significantly in size and you want to balance load more carefully.

Explicit Assignment

csharp
x.UseExplicitAssignment();

Requires all tenants to be pre-assigned to a database via the admin API before they can be used. Any attempt to use an unrecognized tenant ID throws an UnknownTenantIdException. This gives you complete control over tenant placement at the cost of requiring an upfront registration step.

Best for: regulated environments where tenant placement must be deliberate, or when you need to co-locate related tenants in the same database.

Custom Strategy

csharp
x.UseCustomAssignment(new MyStrategy());

Implement the ITenantAssignmentStrategy interface from Weasel.Core.MultiTenancy:

csharp
public interface ITenantAssignmentStrategy
{
    ValueTask<string> AssignTenantToDatabaseAsync(
        string tenantId,
        IReadOnlyList<PooledDatabase> availableDatabases);
}

The strategy is called under a PostgreSQL advisory lock, so it does not need to handle concurrency itself. The availableDatabases list only includes databases that are not marked as full.

Database Registry Tables

The sharded tenancy model uses two tables in the master database to track the pool and tenant assignments:

mt_database_pool — registry of all databases in the pool:

ColumnTypeDescription
database_idVARCHAR (PK)Unique identifier for the database
connection_stringVARCHAR NOT NULLPostgreSQL connection string
is_fullBOOLEAN NOT NULL DEFAULT falseWhen true, no new tenants are assigned here
tenant_countINTEGER NOT NULL DEFAULT 0Number of tenants currently assigned

mt_tenant_assignments — maps each tenant to its assigned database:

ColumnTypeDescription
tenant_idVARCHAR (PK)The tenant identifier
database_idVARCHAR NOT NULL (FK)References mt_database_pool.database_id
assigned_atTIMESTAMPTZ NOT NULL DEFAULT now()When the assignment was made

These tables are created automatically when AutoCreateSchemaObjects is enabled.

Admin API

Marten provides an admin API on IDocumentStore.Advanced for managing the database pool and tenant assignments at runtime. All mutating operations acquire a PostgreSQL advisory lock on the master database to prevent concurrent corruption.

Adding Tenants

csharp
// Auto-assign a tenant using the configured strategy
// Returns the database_id the tenant was assigned to
var dbId = await store.Advanced.AddTenantToShardAsync("new-tenant", ct);

// Explicitly assign a tenant to a specific database
await store.Advanced.AddTenantToShardAsync("vip-tenant", "shard_01", ct);

When a tenant is assigned, Marten automatically creates native PostgreSQL LIST partitions for that tenant in the target database across all multi-tenanted document tables and event tables.

Managing the Database Pool

csharp
// Add a new database to the pool at runtime
await store.Advanced.AddDatabaseToPoolAsync("shard_05", newConnectionString, ct);

// Mark a database as full — no new tenants will be assigned to it
await store.Advanced.MarkDatabaseFullAsync("shard_01", ct);

Marking a database as full is useful when a database is approaching capacity limits. Existing tenants in that database continue to work normally, but all new tenant assignments will go to other databases.

Implicit Assignment

If you are using the hash or smallest strategy, you do not need to explicitly add tenants. When a session is opened for an unknown tenant ID, Marten will automatically:

  1. Acquire an advisory lock on the master database
  2. Check if another process already assigned the tenant (double-check after lock)
  3. Run the assignment strategy to pick a database
  4. Write the assignment to mt_tenant_assignments
  5. Create list partitions in the target database
  6. Release the lock and return the session

This means your application code can simply use store.LightweightSession("any-tenant-id") and Marten handles the rest.

Async Daemon Support

The async daemon automatically discovers all databases in the pool through BuildDatabases() and runs asynchronous projections across all of them. When new databases or tenants are added at runtime, the daemon's periodic health check picks them up and starts projection processing without any downtime or reconfiguration.

Dynamically applying changes to tenants databases

If you didn't call the ApplyAllDatabaseChangesOnStartup method, Marten would still try to create a database upon the session creation. This action is invasive and can cause issues like timeouts, cold starts, or deadlocks. It also won't apply all defined changes upfront (so, e.g. indexes, custom schema extensions).

If you don't know the tenant upfront, you can create and apply changes dynamically by:

cs
var tenant = await theStore.Tenancy.GetTenantAsync(tenantId);
await tenant.Database.ApplyAllConfiguredChangesToDatabaseAsync();

snippet source | anchor

You can place this code somewhere in the tenant initialization code. For instance:

  • tenant setup procedure,
  • dedicated API endpoint
  • custom session factory, although that's not recommended for the reasons mentioned above.

Write your own tenancy strategy!

TIP

It is strongly recommended that you first refer to the existing Marten options for per-database multi-tenancy before you write your own model. There are several helpers in the Marten codebase that will hopefully make this task easier. Failing all else, please feel free to ask questions in the Marten's Discord channel about custom multi-tenancy strategies.

The multi-tenancy strategy is pluggable. Start by implementing the Marten.Storage.ITenancy interface:

cs
/// <summary>
///     Pluggable interface for Marten multi-tenancy by database
/// </summary>
public interface ITenancy: IDatabaseSource, IDisposable, IDatabaseUser
{
    /// <summary>
    ///     The default tenant. This can be null.
    /// </summary>
    Tenant Default { get; }

    /// <summary>
    ///     A composite document cleaner for the entire collection of databases
    /// </summary>
    IDocumentCleaner Cleaner { get; }

    /// <summary>
    ///     Retrieve or create a Tenant for the tenant id.
    /// </summary>
    /// <param name="tenantId"></param>
    /// <exception cref="UnknownTenantIdException"></exception>
    /// <returns></returns>
    Tenant GetTenant(string tenantId);

    /// <summary>
    ///     Retrieve or create a tenant for the tenant id
    /// </summary>
    /// <param name="tenantId"></param>
    /// <returns></returns>
    ValueTask<Tenant> GetTenantAsync(string tenantId);

    /// <summary>
    ///     Find or create the named database
    /// </summary>
    /// <param name="tenantIdOrDatabaseIdentifier"></param>
    /// <returns></returns>
    ValueTask<IMartenDatabase> FindOrCreateDatabase(string tenantIdOrDatabaseIdentifier);

    /// <summary>
    ///     Find or create the named database
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    ValueTask<IMartenDatabase> FindDatabase(DatabaseId id)
    {
        throw new NotImplementedException("You will need to implement this interface method to use a Marten store with Wolverine projection/subscription distribution");
    }

    /// <summary>
    ///  Asserts that the requested tenant id is part of the current database
    /// </summary>
    /// <param name="database"></param>
    /// <param name="tenantId"></param>
    bool IsTenantStoredInCurrentDatabase(IMartenDatabase database, string tenantId);
}

snippet source | anchor

Assuming that we have a custom ITenancy model:

cs
// Make sure you implement the Dispose() method and
// dispose all MartenDatabase objects
public class MySpecialTenancy: ITenancy

snippet source | anchor

We can utilize that by applying that model at configuration time:

cs
var store = DocumentStore.For(opts =>
{
    opts.Connection("connection string");

    // Apply custom tenancy model
    opts.Tenancy = new MySpecialTenancy();
});

snippet source | anchor

Administering Multiple Databases

We've tried to make Marten support all the existing IDocumentStore.Advanced features either across all databases at one time where possible, or exposed a mechanism to access only one database at a time as shown below:

cs
// Apply all detected changes in every known database
await store.Storage.ApplyAllConfiguredChangesToDatabaseAsync();

// Only apply to the default database if not using multi-tenancy per
// database
await store.Storage.Database.ApplyAllConfiguredChangesToDatabaseAsync();

// Find a specific database
var database = await store.Storage.FindOrCreateDatabase("tenant1");

// Tear down everything
await database.CompletelyRemoveAllAsync();

// Check out the projection state in just this database
var state = await database.FetchEventStoreStatistics();

// Apply all outstanding database changes in just this database
await database.ApplyAllConfiguredChangesToDatabaseAsync();

snippet source | anchor

All remaining methods on IDocumentStore.Advanced apply to all databases.

Released under the MIT License.