Skip to main content

Multi-tenancy support in SQL Persistence

Multi-tenant systems are a popular way to use the same codebase to provide services to different customers while minimizing the effect they have on each other. In a distributed message-based system you need to partition customer information and segregate messages from different customers as well. Additionally, you have to make sure different system components are tenant-aware.

In NServiceBus SQL Persistence 4.6, we have added new features that make it a bit easier to create multi-tenant systems. Let's see how it all works.

Flowing tenant identifier

The basic building block of a multi-tenant system is the notion of a Tenant Identifier, which is passed from message to message so that it flows through an entire conversation of messages. Once you flow this TenantId, you know the active tenant in the context of the running code.

In a distributed message-based system, you begin when the conversation or the workflow starts. Usually, when the first message that initiates a conversation is sent, you know who is triggering it, either via an external endpoint, or the data available at runtime. That allows setting the TenantId as a message header. We use a message header for this purpose because the tenant identifier is not part of the message itself, but an infrastructure concern.

As an example, when sending a message from a WebAPI endpoint, the code would look something like the following snippet. Note that there’s no tenant information in the message body:

var options = new SendOptions();
options.SetHeader("tenant_id", tenantId);
await endpointInstance.Send(new PlaceOrder(), options)
    .ConfigureAwait(false);

From this point on, you can access the header and read back the TenantId further down in your message handling pipeline. On systems that rely minimally on the tenant information, doing this manually might be enough. However, the better approach is to automate it with a pair of incoming and outgoing behaviors that propagate the TenantId from an incoming message to any outgoing messages sent by the message handler.

As long as we have these infrastructure behaviors, we have seamless access to the tenant information throughout the system. The message processing pipeline with automated behaviors would look like the following diagram:

Message Processing Pipeline

Tenant data isolation

When hosting multiple tenants, proper data isolation is a key ingredient. One common (but problematic) approach is to store everything in one single database with a discriminator column (like CustomerId) to separate tenant-specific data. This method can be dangerous: it is not possible to treat important customers differently–for example to move them to a faster database–even if the business demands it.

A better approach is to have a dedicated database for each tenant, which provides ideal isolation and allows fine-tuning each database separately.

The new version of SQL Persistence in NServiceBus now allows you to set up an automatic mapping from the tenant identifier in the message header to the tenant database connection string, so that the database-per-tenant pattern can be achieved. In this mode, the SQL Persistence stores saga and outbox data in the same tenant database so they are also isolated.

Setup is straightforward, requiring a factory like this:

var persistence = endpointConfiguration.UsePersistence<SqlPersistence>();
persistence.MultiTenantConnectionBuilder(tenantIdHeaderName: "tenant_id",
    buildConnectionFromTenantData: tenantId =>
    {
        var connection = $@"Data Source=.\SqlExpress;Initial Catalog=DatabaseForTenant_{tenantId};Integrated Security=True";
        return new SqlConnection(connection);
    });

There are also more complex ways to construct a tenant connection, such as using multiple headers to determine the tenant, which are covered in the documentation.

Tenant-aware components

Good data isolation, along with tenant information flowing automatically through all messages, makes a very good foundation for a multi-tenant system, but we can do even more. We can piggyback on the incoming/outgoing behaviors and inject tenant-aware components into the system so we don't need to litter our code with lots of conditional tenant-based checks.

Imagine a system where you have different business rules for each tenant. Trying to embed those rules directly in message handlers would make for some messy, hard-to-maintain code:

class PlaceOrderHandler : IHandleMessages<PlaceOrder>
{
    public async Task Handle(PlaceOrder message, IMessageHandlerContext context)
    {
        var tenant = context.MessageHeaders["tenant_id"];
        var tenantRules = null;

        switch(tenant) 
        {
            case "TenantA":
                tenantRules = LoadRulesForTenantA();
                break;
            case "TenantB":
                tenantRules = LoadRulesForTenantB();
                break;
            default:
                throw new NotImplementedException("Rules for " + tenant + " was not implemented");
        }
        
        if (tenantRules.ShouldDispatchOrderImmediately())
        {
            await context.Send(new DispatchOrder())
                .ConfigureAwait(false);
        }
    }
}

A better approach is to separate the concerns where your infrastructure code does the plumbing work and your handler is centered around the business logic. This can be achieved using a behavior in the message processing pipeline, similar to the ones mentioned above. The behavior would set up a tenant-specific component for use while processing a message, based on the TenantId that message contains. The message handler above after the refactoring would look like this:

class PlaceOrderHandler : IHandleMessages<PlaceOrder>
{
    ITenantRuleComponent tenantRuleComponent;

    public MyMessageHandler(ITenantRuleComponent tenantRuleComponent)
    {
        // Injected component knows the TenantId in the message being processed
        this.tenantRuleComponent = tenantRuleComponent;
    }

    public async Task Handle(PlaceOrder message, IMessageHandlerContext context)
    {
        if (tenantRuleComponent.ShouldDispatchOrderImmediately())
        {
            await context.Send(new FurtherMessages())
                .ConfigureAwait(false);
        }
    }
}

Our Injecting tenant-aware components to message handlers sample explains this approach in more detail.

Summary

Developing effective multi-tenant systems is hard. You must distinguish which messages belong to which tenant, determine what data belongs to whom, and always be aware of the current tenant when executing code. The new 4.6 version of SQL Persistence in NServiceBus along with the multi-tenant samples we've created should make your multi-tenant systems development just that little bit easier.


About the author: Hadi Eskandari is a developer at Particular Software who dreams of web-scale system architecture.

Don't miss a thing. Sign up today and we'll send you an email when new posts come out.
Thank you for subscribing. We'll be in touch soon.
 
We collect and use this information in accordance with our privacy policy.
Need help getting started?