I have been thinking about web based multi-tenant SaaS application architectures in my downtime lately and one of the problems that I wanted to investigate was how to route tenant specific requests to tenant specific databases.
One of the scaling challenges in such applications is that of database partioning/sharding. In regular applications there is usually one database and one codebase, all the requests go to the same application database and therefore its fairly easy to just configure the database connection in some sort of config file once and then the repository that’s using the DbContext (for this post assuming Entity Framework and ASP.NET Core) can just connect to that database. The data in the database is logically partitioned by user. Roughly represented, it looks like this:

However, in a true multi-tenant environment there may only be a single codebase running (although I have seen a case where there was one application instance per tenant) but the application database is usually physically sharded/partitioned by tenants, although there are other styles available as well. Partitioning is done to achieve isolation and scalability mainly i.e. if one tenant is doing something rather resource intensive against their database it’s likely not going to affect any other tenants’ requests to their own database. These databases can also be replicated for availability reasons. I would highly recommend downloading/reading this case study on MSDN about multi-tenant apps on Azure, the product is fictional but the architectural concerns are real.
The problem of course (at least for me) is how do you route tenant requests to the correct databases given that you have only one codebase and multiple tenant specific databases?

In this post I present one way I think this problem could be solved, there might be better ways to do this and I am sure to change my views later but this approach will do for now. The gist of the solution is:
- The connection string stored in the config should not contain the name of any specific database but only a placeholder because I will set this at runtime. For this solution, I chose to name the tenant databases after the tenant names. Tenant provisioning is beyond the scope of this post.
- Build a custom middleware component in ASP.NET Core that will intercept tenant requests.
- Extract the tenant name from the request for e.g. tenanta.myapp.com has the tenant name (TenantA) in the host part of the URL. However, in my simple localhost based solution, I am using the URL path instead for e.g. localhost:1234/tenanta/home/index.
- Inject the tenant name into a DbContext factory instance that will be dependency injected into the repository later.
- Invoke the factory from the repository that will then replace the tenant placeholder in the connection string template with the injected tenant name and form the correct connection string at runtime. It will do this for each tenant request.
{ | |
"ConnectionStringTemplate": | |
"Server=(localdb)\\mssqllocaldb;Database={tenant};"+ | |
"Trusted_Connection=True;MultipleActiveResultSets=true" | |
} |
NB: the “+” is just for display purposes here, the string will be a single unbroken string in the appSettings.Development.json file in the web project. The {tenant} placeholder is what will dynamically get replaced by the actual database name which in this case is the same as the tenant name in the URL. You could make the naming convention as sophisticated as you want but this principal should still hold.
For this post, I created a rough and tumble “multi-tenanted” application architecture in Visual Studio using ASP.NET Core MVC and DDD-lite:

In the DataAccess assembly – for the purposes of this example only – I created a simple anemic domain model of a Customer with a few fields for this application whose objective it is to maintain a list of customers for tenants i.e. organisations that use my Software as a Service application. From this domain model and CRMContext class I then created a database for TenantA using EF core code first migrations and then replicated the schema for TenantB using generated SQL from TenantA’s database. For this small demo sample, I am just using LocalDB within Visual Studio.

I then created a simple Repository interface and its implementation that wrapped my CRMContext and exposed one method to load all customers. I am taking a dependency on a IDbContextFactory interface that I created in my DataAccess project. Since the goal is to construct the DbContext instance dynamically based on the tenant name so we can route tenant queries to the appropriate database.
public class CustomerRepository : ICustomerRepository | |
{ | |
private CRMContext context; | |
public CustomerRepository(IDbContextFactory dbContextFactory) | |
{ | |
this.context = dbContextFactory.Create(); | |
} | |
public IEnumerable GetAllCustomers() | |
{ | |
return this.context?.Customers.ToList().AsEnumerable(); | |
} | |
} |
public interface IDbContextFactory | |
{ | |
string TenantName { get; set; } | |
CRMContext Create(); | |
} |
The DbContextFactory below will replace the tenant placeholder in the connection string template with the actual tenant name passed through the URL and use it to instantiate and return an instance of the CRMContext.
DISCLAIMER: Please bear in mind, this implementation of IDbContextFactory only applies to Entity Framework backed repositories, if you are using some other ORM you will have to create a different implementation of the IDbContextFactory and figure out how to construct the appropriate instance of the context in that ORM for e.g. NHibernate’s Session object. I haven’t gone that far for this post for now but may be in time. Also, you might have to create another implementation for NoSQL based data stores and establish the logic to connect to the store dynamically.
public class DbContextFactory : IDbContextFactory | |
{ | |
private string connectionStringTemplate; | |
public string TenantName { get; set; } | |
public DbContextFactory(string connectionStringTemplate) | |
{ | |
this.connectionStringTemplate = connectionStringTemplate; | |
} | |
public CRMContext Create() | |
{ | |
CRMContext context = null; | |
if (!string.IsNullOrWhiteSpace(this.TenantName)) | |
{ | |
var dbContextOptionsBuilder = new DbContextOptionsBuilder(); | |
dbContextOptionsBuilder.UseSqlServer(this.connectionStringTemplate | |
.Replace("{tenant}", this.TenantName)); | |
context = new CRMContext(dbContextOptionsBuilder.Options); | |
} | |
return context; | |
} | |
} |
I then created an application services project to abstract my MVC controllers from the repository. I put just one method in it for now that loads all the customers:
Then finally, in the HomeController I took a dependency on this ICustomerService interface. Note how none of the higher layers know anything about tenants at this point, most of that concern has been tucked away in the repository and the factory. This was the key goal throughout this exercise, to minimise leaky abstractions.
public class HomeController : Controller | |
{ | |
private readonly ICustomerService customerSvc; | |
public HomeController(ICustomerService customerService) | |
{ | |
this.customerSvc = customerService; | |
} | |
public IActionResult Index() | |
{ | |
List customersForTenant = new List(); | |
var customers = this.customerSvc.GetAllCustomers(); | |
// ... return results | |
} | |
} |
Next, I created a custom middleware component that intercepts tenant requests. Here, I am extracting the tenant name part of the URL and injecting into the IDbContextFactory instance. I wanted to ensure that the path part is parsed only in debug mode and host part in release mode hence the pragma directives.
public class TenantDBMappingMiddleware | |
{ | |
private readonly RequestDelegate next; | |
public TenantDBMappingMiddleware(RequestDelegate next) | |
{ | |
this.next = next; | |
} | |
public async Task Invoke(HttpContext httpContext) | |
{ | |
string[] urlParts = null; | |
#if DEBUG | |
urlParts = httpContext.Request.Path.Value.Split(new char[] { '/' }, | |
StringSplitOptions.RemoveEmptyEntries); | |
#else | |
urlParts = httpContext.Request.Host.Host.Split(new char[] { '.' }, | |
StringSplitOptions.RemoveEmptyEntries); | |
#endif | |
if (urlParts != null && urlParts.Any()) | |
{ | |
// Request the previously configured IDbContextFactory instance | |
// ASP.NET Core's Dependency Injection container | |
httpContext.RequestServices.GetService().TenantName = urlParts[0]; | |
} | |
await this.next(httpContext); | |
} | |
} |
This instance of IDbContextFactory was already added as a service in the ConfigureServices() method in the Startup.cs.
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddScoped(); | |
services.AddScoped(); | |
services.AddScoped(serviceProvider => | |
new | |
DbContextFactory(Configuration["ConnectionStringTemplate"])); | |
// .. more services | |
} |
Finally, I created a little extension method that I can call from the Configure() method in the Startup.cs, to easily add my middleware into the application pipeline. Note that this call goes just before the call to UseMvc().
public static class TenantDBMappingMiddlewareExtensions | |
{ | |
public static IApplicationBuilder UseTenantDBMapper(this IApplicationBuilder | |
applicationBuilder) | |
{ | |
return applicationBuilder.UseMiddleware<TenantDBMappingMiddleware>(); | |
} | |
} |
public void Configure(IApplicationBuilder app, IHostingEnvironment env) | |
{ | |
// ..other middleware | |
app.UseTenantDBMapper(); | |
app.UseMvc(routes => | |
{ | |
routes.MapRoute( | |
name: "default", | |
template: "{tenant}/{controller=Home}/{action=Index}/{id?}"); | |
}); | |
} |
In order to make sure that MVC routing can understand the tenant-ed URLs I changed the default route to add tenant parameter as well.
That’s pretty much it!
The only problem with running this solution on localhost is that if your URL doesn’t have the tenant name part in it, the middleware will inject the default controller’s name into the DbContextFactory i.e. “Home” in this case which will result in the Repository trying to make a connection to the “Home” database which doesn’t exist so it will throw an exception. In a real multi-tenant application this should not happen because the tenant name will be a part of the host part instead of the path part of the URL for e.g. tenantname.myapp.com. There are several ways to design tenant URLs in a multi-tenant app, I would recommend reading this to get a better understanding of the options.
To make it work on localhost, one (imperfect) solution could be to set the launch URL in the web project properties to point to a known tenant, for e.g. in my case “TenantA”:

At least this way, local development and testing can still proceed. The other way could be to put a login screen on the home page and upon login navigate to a known tenant URL. You will probably have at least one tenant database that you will be developing and testing against and that should be enough to make this work locally. Of course, before deploying it you will need to un-check this option to let the host based tenant routing take over.
Like I said, this is not a perfect solution and might have loopholes that I haven’t addressed in this post, but its a modestly good starting point. In future posts, I might revisit this with an even better solution or if anyone reading this has one, I will be more than happy to learn about it.
[UPDATE 12-June-2018]: on popular demand, here is the code for this PoC. Ideas and suggestions always welcome!
Can you please share the source code this sample?
There is not much more than what I have shown here really. The application itself doesn’t do much other than load a list of rows from a table based on which tenant it ends up connecting to. Most of the stuff is infrastructure.
Can you share the code please.
Link to the GitHub code repo at the bottom of the post. Cheers!
Excellent article. Thank you for sharing the code.
this is my current implementation. But the biggest challenge i am facing is how to secure it if i dont want to use any backed Authentication mechanism but still want to serve T1=TA, T2=TB.. at the same time i dont want to do this T=TB, T2=TA, I have used tenant name in URL instead of Hostname parsing.