Loading tenants from the database with SaasKit in ASP.NET Core

Building a multi-tenant application can be a difficult thing to get right - it's normally critical that there is no leakage between tenants, where one tenant sees details from another. In the previous version of ASP.NET this problem was complicated by the multiple extension points you needed to hook into to inject your custom behaviour.

With the advent of ASP.NET Core and the concept of the 'Middleware pipeline', modelled after the OWIN interface, this process becomes a little easier. The excellent open source project SaasKit, by Ben Foster, makes adding multi-tenancy to your application a breeze. He has a number of posts on building multi-tenant applications on his blog, which I recommend checking out. In particular, his post here gave me the inspiration to try out SaasKit, and write this post.

In his post, Ben describes how to add middleware to your application to resolve the tenant for a given hostname from a list provided in appsettings.json. As an extension to this, rather than having a fixed set of tenants loaded at start up, I wanted to be able to resolve the tenants at runtime from a database.

In this post I'll show how to add multi-tenancy to an ASP.NET Core application where the tenant mapping is stored in a database. I'll be using the cross platform PostgreSQL database (see my previous post for configuring PostreSQL on OS X) but you can easily use a different database provider.

The Setup

First create a new ASP.NET Core application. We are going to be loading our tenants from the database using Entity Framework Core, so you will need to add a database provider and your connection string.

Once you're database is all configured, we will create our AppTenant entity. Tenants can be split in multiple ways, basically based on any consistent property of a request (e.g. hostname, headers etc). We will be use a hostname per tenant, so our AppTenant class looks like this:

We can add a migration and update our database with our new entity using the Entity Framework tools:

$ dotnet ef migrations add AddAppTenantEntity

$ dotnet ef database update

Resolving Tenants

We are using SaasKit to simplify our tenant handling so we will need to add SaasKit.Multitenancy to our project.json:

{"dependencies":{
...
"SaasKit.Multitenancy":"1.1.4",
...
},}

Next we can add an implementation of an ITenantResolver<AppTenant>. This class will be used to identify the associated tenant for a given request. If a tenant is found it returns a TenantContext<AppTenant>, if no tenant can be resolved it returns null.

In this implementation we just find the first AppTenant in the database with the provided HostName - you can obviously match on any parameter here depending on your AppTenant definition.

Configuring the services and Middleware

Now we have defined our AppTenant and a way of resolving a tenant from the database, we just need to wire this all up in to to our applications.

As with most ASP.NET Core components we need to register the dependent services and the middleware in our Startup class. First we configure our tenant class and resolver with the AddMultitenancy extension method on the IServiceCollection:

Finally we setup our middleware to resolve our tenant. The order of middleware components is important - we add the SaasKit middleware early in the pipeline, just after the static file middleware.

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.publicvoidConfigure(IApplicationBuilder app,IHostingEnvironment env,ILoggerFactory loggerFactory){// if astatic file is requested, serve that without needing to resolve a tenant from the db first.
app.UseStaticFiles();
app.UseMultitenancy<AppTenant>();// other middleware}

Setup app to listen on multiple urls

In order to have a multi-tenant app based on hostname, we need to update our app to actually listen on multiple urls. How to do this will depend on how you are hosting your app.

For now I will configure Kestrel to listen on three urls by specifying them directly in our WebHostBuilder. In production you would definitely want to configure this using a different approach so the urls are not hard coded - otherwise we will not be getting any benefit of storing tenants in the database!

If we run the app now, the AppTenant is resolved from the database based on the current hostname. However currently we aren't actually using the tenant anywhere so running the app just gives us the default view no matter which url we hit:

Injecting the current tenant

To prove that our AppTenant has been resolved, we will inject it into _Layout.cshtml and use it to change the title in the navigation bar.

The ability to inject arbitrary services directly into view pages is new in ASP.NET Core and can be useful for injecting view specific services. An AppTenant is not view-specific, so it is more likely to be required in the Controller rather than the View, however view injection is a useful mechanism for demonstration here.

First we add the @inject statement to the top of _Layout.cshtml:

@inject DatabaseMultiTenancyWithSaasKit.Models.AppTenant Tenant;

We now effectively have a property Tenant we can reference later in the layout page:

Now when we navigate to the various registered urls, we can see the current AppTenant has been loaded from the database, and it's Name displayed in the navigation bar:

##Summary

One of the first steps in any multi-tenant application is identifying which tenant the current request is related to. In this post we used the open source SaasKit to resolve tenants based on the current request hostname. We designed the resolver to load the tenants from a database, so that we could dynamically add new tenants at runtime. We then used service injection to show that the AppTenant object representing our current tenant can be sourced from the dependency injection container. The source code for the above example can be found here.

If you are interested in building multi-tenancy apps with SaasKit I highly recommend you check out Ben Foster's blog at benfoster.io for more great examples.