Multitenancy

ServiceStack provides a number of ways of changing the database connection used at runtime based on an incoming Request. You can use a Request Filter, use the [ConnectionInfo] Request Filter Attribute, use the [NamedConnection] attribute on Auto Query Services, access named connections in Custom Service implementations or override GetDbConnection(IRequest) in your AppHost.

Change Database Connection at Runtime

The default implementation of IAppHost.GetDbConnection(IRequest) includes an easy way to change the DB Connection that can be done by populating the ConnectionInfo POCO in any Request Filter in the Request Pipeline:

req.Items[Keywords.DbInfo] = new ConnectionInfo {
    NamedConnection  = ... //Use a registered NamedConnection for this Request
    ConnectionString = ... //Use a different DB connection for this Request
    ProviderName     = ... //Use a different Dialect Provider for this Request
};

To illustrate how this works we'll go through a simple example showing how to create an AutoQuery Service that lets the user change which DB the Query is run on. We'll control which of the Services we want to allow the user to change the DB it's run on by having them implement the interface below:

public interface IChangeDb
{
    string NamedConnection { get; set; }
    string ConnectionString { get; set; }
    string ProviderName { get; set; }
}

We'll create one such AutoQuery Service, implementing the above interface:

[Route("/rockstars")]
public class QueryRockstars : QueryBase<Rockstar>, IChangeDb
{
    public string NamedConnection { get; set; }
    public string ConnectionString { get; set; }
    public string ProviderName { get; set; }
}

For this example we'll configure our Database to use a default SQL Server 2012 database, register an optional named connection looking at a "Reporting" PostgreSQL database and register an alternative Sqlite RDBMS Dialect that we also want the user to be able to use:

ChangeDB AppHost Registration

container.Register<IDbConnectionFactory>(c => 
    new OrmLiteConnectionFactory(defaultDbConn, SqlServer2012Dialect.Provider));

var dbFactory = container.Resolve<IDbConnectionFactory>() as OrmLiteConnectionFactory;

//Register NamedConnection
dbFactory.RegisterConnection("Reporting", ReportConnString, PostgreSqlDialect.Provider);

//Register DialectProvider
dbFactory.RegisterDialectProvider("Sqlite", SqliteDialect.Provider);

ChangeDB Request Filter

To enable this feature we just need to add a Request Filter that populates the ConnectionInfo with properties from the Request DTO:

GlobalRequestFilters.Add((req, res, dto) => {
   var changeDb = dto as IChangeDb;
   if (changeDb == null) return;

   req.Items[Keywords.DbInfo] = new ConnectionInfo {
       NamedConnection = changeDb.NamedConnection,
       ConnectionString = changeDb.ConnectionString,
       ProviderName = changeDb.ProviderName,
   };
});

Since our IChangeDb interface shares the same property names as ConnectionInfo, the above code can be further condensed using a Typed Request Filter and ServiceStack's built-in AutoMapping down to just:

RegisterTypedRequestFilter<IChangeDb>((req, res, dto) =>
    req.Items[Keywords.DbInfo] = dto.ConvertTo<ConnectionInfo>());

Change Databases via QueryString

With the above configuration the user can now change which database they want to execute the query on, e.g:

var response = client.Get(new QueryRockstars()); //SQL Server

var response = client.Get(new QueryRockstars {   //Reporting PostgreSQL DB
    NamedConnection = "Reporting"
}); 

var response = client.Get(new QueryRockstars {   //Alternative SQL Server Database
    ConnectionString = "Server=alt-host;Database=Rockstars;User Id=test;Password=test;"
}); 

var response = client.Get(new QueryRockstars {   //Alternative SQLite Database
    ConnectionString = "C:\backups\2016-01-01.sqlite",
    ProviderName = "Sqlite"
}); 

ConnectionInfo Attribute

To make it even easier to use we've also wrapped this feature in a simple ConnectionInfoAttribute.cs which allows you to declaratively specify which database a Service should be configured to use, e.g we can configure the Db connection in the Service below to use the PostgreSQL Reporting database with:

[ConnectionInfo(NamedConnection = "Reporting")]
public class ReportingServices : Service
{
    public object Any(Sales request)
    {
        return new SalesResponse { Results = Db.Select<Sales>() };
    }
}

Auto Query Named Connection

Auto Query can also easily be configured to query any number of different databases registered in your AppHost.

In the example below we configure our main RDBMS to use SQL Server and register a Named Connection to point to a Reporting PostgreSQL RDBMS:

var dbFactory = new OrmLiteConnectionFactory(connString, SqlServer2012Dialect.Provider);
container.Register<IDbConnectionFactory>(dbFactory);

dbFactory.RegisterConnection("Reporting", pgConnString, PostgreSqlDialect.Provider);

Any normal AutoQuery Services like QueryOrders will use the default SQL Server connection whilst QuerySales will execute its query on the PostgreSQL Reporting Database instead:

public class QueryOrders : QueryDb<Order> {}

[ConnectionInfo(NamedConnection = "Reporting")]
public class QuerySales : QueryDb<Sales> {}

An alternative to specifying the [ConnectionInfo] Request Filter Attribute on the AutoQuery Request DTO, is to specify the named connection on the POCO Table instead, e.g:

[NamedConnection("Reporting")]
public class Sales { ... }

public class QuerySales : QueryDb<Sales> {}

Which can also be added on the Request DTO itself:

[NamedConnection("Reporting")]
public class ViewSales : IReturn<ViewSalesResponse> { }

Resolving Named Connections in Services

Whilst inside a Service you can change which DB connection to use by passing in the NamedConnection when opening a DB Connection. E.g. The example below allows the user to change which database to retrieve all sales records for otherwise fallbacks to "Reporting" database by default:

public class SalesServices : Service
{
   public IDbConnectionFactory ConnectionFactory { get; set; } 

   public object Any(GetAllSales request)
   {
       var namedConnection = request.NamedConnection ?? "Reporting";
       using (var db = ConnectionFactory.Open(namedConnection)) 
       {
           return db.Select<Sales>();
       }
   }
}

Override Connection used per request at Runtime

All built-in dependencies available from Service base class, AutoQuery, Razor View pages, etc are resolved from a central overridable location in your AppHost. This lets you control which pre-configured dependency gets used based on the incoming Request for each Service by overriding any of the AppHost methods below:

public IDbConnection Db => db ??= HostContext.AppHost.GetDbConnection(Request);

public IRedisClient Redis => redis ??= HostContext.AppHost.GetRedisClient(Request);

public ICacheClient Cache => cache ??= HostContext.AppHost.GetCacheClient(Request);

public MemoryCacheClient LocalCache => localCache ??= HostContext.AppHost.GetMemoryCacheClient(Request);

public IMessageProducer MessageProducer => messageProducer ??= 
    HostContext.AppHost.GetMessageProducer(Request);

E.g. to change the DB Connection your Service uses you can override GetDbConnection(IRequest) in your AppHost.

Multitenancy RDBMS AuthProvider

ServiceStack resolves its IAuthProvider from the overridable GetAuthRepository(IRequest) AppHost factory method just like the other "Multitenancy-aware" dependencies above letting you dynamically change which AuthProvider should be used based on the incoming request.

This can be used with the new OrmLiteAuthRepositoryMultitenancy provider to maintain isolated User Accounts per tenant in all major supported RDBMS

Since each tenant database uses their own isolated UserAuth tables we need to provide the list of db connection strings that the OrmLite AuthRepository uses to check and create any missing User Auth tables:

var connectionStrings = 100.Times(i => GetConnectionStringForTenant(i));
container.Register<IAuthRepository>(c =>
    new OrmLiteAuthRepositoryMultitenancy(c.TryResolve<IDbConnectionFactory>(),
        connectionStrings));

container.Resolve<IAuthRepository>().InitSchema(); // Create any missing UserAuth tables

However if you've already created all UserAuth table schema's for each tenant or are manually creating them out-of-band you can register it without the list of connection strings:

container.Register<IAuthRepository>(c =>
    new OrmLiteAuthRepositoryMultitenancy(c.TryResolve<IDbConnectionFactory>()));

Then to specify which AuthRepository should be used for each request we can override GetAuthRepository() in your AppHost and return the OrmLiteAuthRepositoryMultitenancy configured to use the same Multitenant DB connection used in that request, e.g:

public override IAuthRepository GetAuthRepository(IRequest req = null)
{
    return req != null
        ? new OrmLiteAuthRepositoryMultitenancy(GetDbConnection(req)) //At Runtime
        : TryResolve<IAuthRepository>();                              //On Startup
}

Now when GetAuthRepository() is called within the context of a request it uses the same Multitenancy DB as your other services, otherwise when called outside (e.g. on Startup) it uses the default IOC Registration configured with the connectionStrings for each Multitenant DB that it can use to create any missing UserAuth table schemas not found in any of the Multitenant databases.

Extending UserAuth tables

In the same way that you can use Custom UserAuth tables in OrmLiteAuthRepository, you can also extend OrmLiteAuthRepositoryMultitenancy to utilize your own custom UserAuth tables with extended fields by configuring them to use its generic class, e.g:

public class MyUserAuth : UserAuth { .... }
public class MyUserAuthDetails : UserAuthDetails { .... }
container.Register<IAuthRepository>(c =>
    new OrmLiteAuthRepositoryMultitenancy<MyUserAuth, MyUserAuthDetails>(c.Resolve<IDbConnectionFactory>()) {
        UseDistinctRoleTables = true
    });

Multi Tenancy Example

To show how easy it is to implement a Multi Tenancy Service with this feature we've added a stand-alone Multi Tenancy AppHost Example showing 2 different ways we can configure a Service to use different databases based on an incoming request.

In this example we've configured our AppHost to use the master.sqlite database as default and registered 3 different named connections referencing 3 different databases. Each database is then initialized with a different row in the TenantConfig table to identify the database that it's in.

public class MultiTenantChangeDbAppHost : AppSelfHostBase
{
    public MultiTenantChangeDbAppHost()
        : base("Multi Tenant Test", typeof (MultiTenantChangeDbAppHost).Assembly) {}

    public override void Configure(Container container)
    {
        container.Register<IDbConnectionFactory>(new OrmLiteConnectionFactory(
            "~/App_Data/master.sqlite".MapAbsolutePath(), SqliteDialect.Provider));

        var dbFactory = container.Resolve<IDbConnectionFactory>();

        const int noOfTenants = 3;

        using (var db = dbFactory.OpenDbConnection()) {
            InitDb(db, "MASTER", "Masters inc.");
        }

        noOfTenants.Times(i => {
            var tenantId = "T0" + (i + 1);
            using var db = dbFactory.OpenDbConnectionString(GetTenantConnString(tenantId));
            InitDb(db, tenantId, "ACME {0} inc.".Fmt(tenantId));
        });

        RegisterTypedRequestFilter<IForTenant>((req,res,dto) => 
            req.Items[Keywords.DbInfo] = new ConnectionInfo { 
                ConnectionString = GetTenantConnString(dto.TenantId)
            }
        );
    }

    public void InitDb(IDbConnection db, string tenantId, string company)
    {
        db.DropAndCreateTable<TenantConfig>();
        db.Insert(new TenantConfig { Id = tenantId, Company = company });
    }

    public string GetTenantConnString(string tenantId) => tenantId != null 
        ? "~/App_Data/tenant-{0}.sqlite".Fmt(tenantId).MapAbsolutePath()
        : null;
}

This example uses only contains a single Service which returns the first result in the TenantConfig table:

public interface IForTenant
{
    string TenantId { get; }
}

public class TenantConfig
{
    public string Id { get; set; }
    public string Company { get; set; }
}

public class GetTenant : IForTenant, IReturn<GetTenantResponse>
{
    public string TenantId { get; set; }
}

public class GetTenantResponse
{
    public TenantConfig Config { get; set; }
}

public class MultiTenantService : Service
{
    public object Any(GetTenant request)
    {
        return new GetTenantResponse
        {
            Config = Db.Select<TenantConfig>().FirstOrDefault(),
        };
    }
}

Calling this Service with a different TenantId value changes which database the Service is configured with:

var client = new JsonServiceClient(Config.AbsoluteBaseUri);

var response = client.Get(new GetTenant()); //= Company: Masters inc. 

var response = client.Get(new GetTenant { TenantId = "T01" }); //= Company: ACME T01 inc.

var response = client.Get(new GetTenant { TenantId = "T02" }); //= Company: ACME T02 inc.

var response = client.Get(new GetTenant { TenantId = "T03" }); //= Company: ACME T03 inc.

client.Get(new GetTenant { TenantId = "T04" }); // throws WebServiceException

An alternative way to support Multitenancy using a Custom DB Factory is available in MultiTenantAppHostTests.cs.

public class MultiTenantCustomDbFactoryAppHost : AppSelfHostBase
{
    public MultiTenantCustomDbFactoryAppHost()
        : base("Multi Tenant Test", typeof(MultiTenantCustomDbFactoryAppHost).Assembly) { }

    public override void Configure(Container container)
    {
        var dbFactory = new OrmLiteConnectionFactory(
            "~/App_Data/master.sqlite".MapAbsolutePath(), SqliteDialect.Provider);

        const int noOfTenants = 3;

        container.Register<IDbConnectionFactory>(c =>
            new MultiTenantDbFactory(dbFactory));

        var multiDbFactory = (MultiTenantDbFactory)container.Resolve<IDbConnectionFactory>();

        using (var db = multiDbFactory.OpenTenant()) {
            InitDb(db, "MASTER", "Masters inc.");
        }

        noOfTenants.Times(i => {
            var tenantId = "T0" + (i + 1);
            using var db = multiDbFactory.OpenTenant(tenantId);
            InitDb(db, tenantId, "ACME {0} inc.".Fmt(tenantId));
        });

        GlobalRequestFilters.Add((req, res, dto) => {
            if (dto is IForTenant forTenant)
                RequestContext.Instance.Items.Add("TenantId", forTenant.TenantId);
        });
    }

    public void InitDb(IDbConnection db, string tenantId, string company)
    {
        db.DropAndCreateTable<TenantConfig>();
        db.Insert(new TenantConfig { Id = tenantId, Company = company });
    }

    public class MultiTenantDbFactory : IDbConnectionFactory
    {
        private readonly IDbConnectionFactory dbFactory;

        public MultiTenantDbFactory(IDbConnectionFactory dbFactory)
        {
            this.dbFactory = dbFactory;
        }

        public IDbConnection OpenDbConnection()
        {
            var tenantId = RequestContext.Instance.Items["TenantId"] as string;
            return OpenTenant(tenantId);
        }

        public IDbConnection OpenTenant(string tenantId = null)
        {
            return tenantId != null
                ? dbFactory.OpenDbConnectionString(
                    "~/App_Data/tenant-{0}.sqlite".Fmt(tenantId).MapAbsolutePath())
                : dbFactory.OpenDbConnection();
        }

        public IDbConnection CreateDbConnection() => dbFactory.CreateDbConnection();
    }
}