Critter Stack Multi-Tenancy

To go along with the Wolverine 1.0 release, I should probably be blogging some introductory, getting started type content. To hell with that though, let’s jump right into the deep end of the pool today! To the best of my knowledge, no other messaging tooling in .NET can span inbox/outbox integration across this kind of multi-tenanted database storage.

Let’s say that you’re wanting to build a system using the full critter stack (Marten + Wolverine) and you need to support multi-tenancy through a database per tenant strategy for some mix of scalability and data segregation. You’d also like to use some of the goodies that comes from Critter Stack that is going to make your development life a whole lot easier and more productive:

Taking everything from a sample application from the Wolverine documentation, we get all of that with this setup:

using Marten;
using MultiTenantedTodoWebService;
using Oakton;
using Oakton.Resources;
using Wolverine;
using Wolverine.Http;
using Wolverine.Marten;

var builder = WebApplication.CreateBuilder(args);

// You do need a "master" database for operations that are
// independent of a specific tenant. Wolverine needs this
// for some of its state tracking
var connectionString = "Host=localhost;Port=5433;Database=postgres;Username=postgres;password=postgres";

// 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);

// This tells both Wolverine & Marten to make
// sure all necessary database schema objects across
// all the tenant databases are up and ready to go
// on application startup
builder.Services.AddResourceSetupOnStartup();

// 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();
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// Let's add in Wolverine HTTP endpoints to the routing tree
app.MapWolverineEndpoints();

return await app.RunOaktonCommands(args);

In the code above, we’re configured Marten to use a database per tenant in its own storage. When we also call IntegrateWithWolverine() to add Wolverine inbox/outbox support — which Wolverine in 1.0 is able to do for each and every single known tenant database.

And that’s a lot of setup, but now check out this sample usage of some message handlers in a fake Todo service:

public static class TodoCreatedHandler
{
    public static void Handle(DeleteTodo command, IDocumentSession session)
    {
        session.Delete<Todo>(command.Id);
    }
    
    public static TodoCreated Handle(CreateTodo command, IDocumentSession session)
    {
        var todo = new Todo { Name = command.Name };
        session.Store(todo);

        return new TodoCreated(todo.Id);
    }
    
    // Do something in the background, like assign it to someone,
    // send out emails or texts, alerts, whatever
    public static void Handle(TodoCreated created, ILogger logger)
    {
        logger.LogInformation("Got a new TodoCreated event for " + created.Id);
    }    
}

You’ll note that at no point in any of that code do you see anything to do with a tenant, or opening a Marten session to the right tenant, or propagating the tenant id from the initial CreateTodo command to the cascaded TodoCreated event. That’s because Wolverine is happily able to do that for you behind the scenes in Envelope metadata as long as the original command was tagged to a tenant id.

To do that, see these examples of invoking Wolverine from HTTP endpoints:

public static class TodoEndpoints
{
    [WolverineGet("/todoitems/{tenant}")]
    public static Task<IReadOnlyList<Todo>> Get(string tenant, IDocumentStore store)
    {
        using var session = store.QuerySession(tenant);
        return session.Query<Todo>().ToListAsync();
    }

    [WolverineGet("/todoitems/{tenant}/complete")]
    public static Task<IReadOnlyList<Todo>> GetComplete(string tenant, IDocumentStore store)
    {
        using var session = store.QuerySession(tenant);
        return session.Query<Todo>().Where(x => x.IsComplete).ToListAsync();
    }

    // Wolverine can infer the 200/404 status codes for you here
    // so there's no code noise just to satisfy OpenAPI tooling
    [WolverineGet("/todoitems/{tenant}/{id}")]
    public static async Task<Todo?> GetTodo(int id, string tenant, IDocumentStore store, CancellationToken cancellation)
    {
        using var session = store.QuerySession(tenant);
        var todo = await session.LoadAsync<Todo>(id, cancellation);

        return todo;
    }

    [WolverinePost("/todoitems/{tenant}")]
    public static async Task<IResult> Create(string tenant, CreateTodo command, IMessageBus bus)
    {
        // At the 1.0 release, you would have to use Wolverine as a mediator
        // to get the full multi-tenancy feature set.
        
        // That hopefully changes in 1.1
        var created = await bus.InvokeForTenantAsync<TodoCreated>(tenant, command);

        return Results.Created($"/todoitems/{tenant}/{created.Id}", created);
    }

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

I think in Wolverine 1.1 or at least a future incremental release of Wolverine that there will be a way to register automatic tenant id detection from an HTTP resource, for for 1.0 developers need to explicitly code and pass that along to Wolverine themselves. Once Wolverine knows the tenant id though, it will happily propagate that automatically to downstream messages. And in all cases, Wolverine is able to use the correct inbox/outbox storage in the current tenant database during message processing so you still have the single, native transaction spanning the application functionality and the inbox/outbox storage.\

Leave a comment