Search code examples
multi-tenantmasstransit

How to implement outbox in multi-tenant environment with separated databases (EF + SqlServer)?


I have very simillar problem as in this StackOverflow question

We have multi-tenant environment with significant number of tenants. Each of them has physically separated database (with different connection string). Solution proposed in above SO question is not optimal in our case because for large num of tenants (for example 100) it will require to have 200 additional BackgroundServices.

In our project we use EntityFramework Core 7 and SQL server.

To outline our context a bit: connection string is stored within TenantContext class which is registered in DI.


public class TenantContext
{
    public string CurrentTenantId { get; set; } = "no-tenant";
    public string? ConnectionStr { get; set; }
}

TenantContext is set while creation of IServiceScope specific for tenant.

internal class TenantServiceScopeFactory : ITenantServiceScopeFactory
{
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public TenantServiceScopeFactory(IServiceScopeFactory serviceScopeFactory)
    {
        _serviceScopeFactory = serviceScopeFactory;
    }
    
    public IServiceScope CreateScope(string tenantId)
    {
        var scope = _serviceScopeFactory.CreateScope();
        var tenantContext = scope.ServiceProvider.GetRequiredService<TenantContext>();
        tenantContext.CurrentTenantId = tenantId;
    
        return scope;
    }
}

DbContext is registered with factory method based on connection string present in TenantContext.


builder.Services.AddDbContext<AppDbContext>((sp, optionsBuilder) =>
{
    var tenantContext = sp.GetRequiredService<TenantContext>();
    optionsBuilder.UseSqlServer(tenantContext.ConnectionStr);
});

That means that if we want to do anything for specific tenant, we need to create IServiceScope with TenantContext set to specific tenant, and based on that DbContext is created.

If we add MassTransit outbox feature

configurator.AddEntityFrameworkOutbox<AppDbContext>(outboxConfigurator => { … });

then it will do his job for no-tenant TenantContext which always will throw exception because there is no connection string for no-tenant database.

We would like to have single outbox background job (BackgroundService) for all those tenants with logic like this:


    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var tenants = new[]
            {
                "tenant1",
                "tenant2"
            };
            
            foreach (var tenant in tenants)
            {
                var tenantScope = _tenantServiceScopeFactory.CreateScope(tenant);
                // do MassTransit outbox delivery based on AppDbContext resolved within tenantScope
            }
            
            await Task.Delay(Interval, cancellationToken);
        }
    }

Is it even achievable in current shape of MassTransit?

Tried solution proposed in: Is it possible to use MassTransit Transactional Outbox in a Multi-Tenant per DB architecture?


Solution

  • If the only issue you're experiencing is the need to have a delivery service for each tenant, you should be able to copy the existing delivery service code and modify it so that it delivers for all of your tenants. This isn't something that would be built into MassTransit. Consulting is available if you need help or would like this built for you in your application.