Skip to content

Multi-Tenancy and Marten

INFO

This functionality was a very late addition just in time for Wolverine 1.0.

Wolverine.Marten fully supports Marten multi-tenancy features. Both "conjoined" multi-tenanted documents and full blown multi-tenancy through separate databases.

Some important facts to know:

  • Wolverine.Marten's transactional middleware is able to respect the tenant id from Wolverine in resolving an IDocumentSession
  • If using a database per tenant(s) strategy with Marten, Wolverine.Marten is able to create separate message storage tables in each tenant Postgresql database
  • With the strategy above though, you'll need a "master" PostgreSQL database for tenant neutral operations as well
  • The 1.0 durability agent is happily able to work against both the master and all of the tenant databases for reliable messaging

Database per Tenant

INFO

All of these samples are taken from the MultiTenantedTodoWebService sample project;

To get started using Wolverine with Marten's database per tenant strategy, configure Marten multi-tenancy as you normally would, but you also need to specify a "master" database connection string for Wolverine as well as shown below:

cs
// Adding Marten for persistence
builder.Services.AddMarten(m =>
    {
        // With multi-tenancy through a database per tenant
        m.MultiTenantedDatabases(tenancy =>
        {
            // You would probably be pulling the connection strings out of configuration,
            // but it's late in the afternoon and I'm being lazy building out this sample!
            tenancy.AddSingleTenantDatabase("Host=localhost;Port=5433;Database=tenant1;Username=postgres;password=postgres", "tenant1");
            tenancy.AddSingleTenantDatabase("Host=localhost;Port=5433;Database=tenant2;Username=postgres;password=postgres", "tenant2");
            tenancy.AddSingleTenantDatabase("Host=localhost;Port=5433;Database=tenant3;Username=postgres;password=postgres", "tenant3");
        });
        
        m.DatabaseSchemaName = "mttodo";
    })
    .IntegrateWithWolverine(masterDatabaseConnectionString:connectionString);

snippet source | anchor

And you'll probably want this as well to make sure the message storage is in all the databases upfront:

cs
builder.Services.AddResourceSetupOnStartup();

snippet source | anchor

Lastly, this is the Wolverine set up:

cs
// Wolverine usage is required for WolverineFx.Http
builder.Host.UseWolverine(opts =>
{
    // This middleware will apply to the HTTP
    // endpoints as well
    opts.Policies.AutoApplyTransactions();
    
    // Setting up the outbox on all locally handled
    // background tasks
    opts.Policies.UseDurableLocalQueues();
});

snippet source | anchor

From there, you should be completely ready to use Marten + Wolverine with usages like this:

cs
// While this is still valid....
[WolverineDelete("/todoitems/{tenant}/longhand")]
public static async Task Delete(
    string tenant, 
    DeleteTodo command, 
    IMessageBus bus)
{
    // Invoke inline for the specified tenant
    await bus.InvokeForTenantAsync(tenant, command);
}

// Wolverine.HTTP 1.7 added multi-tenancy support so
// this short hand works without the extra jump through
// "Wolverine as Mediator"
[WolverineDelete("/todoitems/{tenant}")]
public static void Delete(
    DeleteTodo command, IDocumentSession session)
{
    // Just mark this document as deleted,
    // and Wolverine middleware takes care of the rest
    // including the multi-tenancy detection now
    session.Delete<Todo>(command.Id);
}

snippet source | anchor

Conjoined Multi-Tenancy

First, let's try just "conjoined" multi-tenancy where there's still just one database for Marten. From the tests, here's a simple Marten persisted document that requires the "conjoined" tenancy model, and a command/handler combination for inserting new documents with Marten:

cs
// Implementing Marten's ITenanted interface
// also makes Marten treat this document type as
// having "conjoined" multi-tenancy
public class TenantedDocument : ITenanted
{
    public Guid Id { get; init; }

    public string TenantId { get; set; }
    public string Location { get; set; }
}

// A command to create a new document that's multi-tenanted
public record CreateTenantDocument(Guid Id, string Location);

// A message handler to create a new document. Notice there's
// absolutely NO code related to a tenant id, but yet it's
// fully respecting multi-tenancy here in a second
public static class CreateTenantDocumentHandler
{
    public static IMartenOp Handle(CreateTenantDocument command)
    {
        return MartenOps.Insert(new TenantedDocument{Id = command.Id, Location = command.Location});
    }
}

snippet source | anchor

For completeness, here's the Wolverine and Marten bootstrapping:

cs
_host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        opts.Services.AddMarten(Servers.PostgresConnectionString)
            .IntegrateWithWolverine()
            .UseLightweightSessions();
        
        opts.Policies.AutoApplyTransactions();
        
    }).StartAsync();

snippet source | anchor

and after that, the calls to InvokeForTenantAsync() "just work" as you can see if you squint hard enough reading this test:

cs
[Fact]
public async Task execute_with_tenancy()
{
    var id = Guid.NewGuid();
    
    await _host.ExecuteAndWaitAsync(c =>
        c.InvokeForTenantAsync("one", new CreateTenantDocument(id, "Andor")));
    
    await _host.ExecuteAndWaitAsync(c =>
        c.InvokeForTenantAsync("two", new CreateTenantDocument(id, "Tear")));
    
    await _host.ExecuteAndWaitAsync(c =>
        c.InvokeForTenantAsync("three", new CreateTenantDocument(id, "Illian")));

    var store = _host.Services.GetRequiredService<IDocumentStore>();

    // Check the first tenant
    using (var session = store.LightweightSession("one"))
    {
        var document = await session.LoadAsync<TenantedDocument>(id);
        document.Location.ShouldBe("Andor");
    }
    
    // Check the second tenant
    using (var session = store.LightweightSession("two"))
    {
        var document = await session.LoadAsync<TenantedDocument>(id);
        document.Location.ShouldBe("Tear");
    }
    
    // Check the third tenant
    using (var session = store.LightweightSession("three"))
    {
        var document = await session.LoadAsync<TenantedDocument>(id);
        document.Location.ShouldBe("Illian");
    }
}

snippet source | anchor

Released under the MIT License.