Multi-Tenant SaaS Architecture with Entity Framework

Developer Partners
10 min readJun 30, 2023

--

Multi-Tenant SaaS Architecture with Entity Framework Core

SaaS solutions are very common. They are the go-to model for software products because of many reasons such as having little maintenance overhead, cost effectiveness, and easy customer onboarding. However, the software architecture you pick for your SaaS solution may decide your success in the business. But before we dive into code for creating a multi-tenant application with Entity Framework and .NET Core, let’s understand what the multi-tenant architecture is. To do that, we have to first understand what the single-tenant architecture is.

What is Single-Tenant Architecture

In the single-tenant software architecture, each customer gets their own version of the compiled application, database, hosting, etc. If there are software updates or patches to push, they have to be done for each customer separately. The following diagram describes the single-tenant architecture:

Single-Tenant Software Architecture
Single-Tenant Software Architecture

The best way to describe what the single-tenant architecture is by an example. Imagine you started a business of creating websites for your clients. You find a few clients and start development of their websites. You create a database for each client. Then you buy hosting for each of your clients and install their websites on those web servers. Everything goes great until you find a bug in your code. How will you fix that bug for all clients? If you have to make database schema changes for fixing that bug, you will have to update each client’s database with those changes. You will have to update your source code, then push the patch to each client’s web server. It may be easy to do for just a few clients, but it will add a lot of maintenance overhead over time as your customer base grows. Imagine your business is doing good, and you have 50 clients now. If there are any changes in your software, you will have to push those changes to all those 50 client websites. I think it’s easy to imagine how much time you have to spend maintaining all those different instances of client websites.

A single-tenant architecture is not bad at all. It just has it’s pros and cons. It may not be the best architecture for a lot of solutions, but there are times when the single-tenant architecture brings a lot of benefits. It all just depends on the nature of your business.

What is Multi-Tenant Architecture

There are different ways to implement the multi-tenant architecture, but the most common way and the one that we are going to create in this article is the type where all customers use the same compiled application, database, hosting, etc. All customer data is stored in the same database and all customers share the same application. It’s almost like they are renting space in your website that is why the term “tenant” is used, so customers or clients are often referred as tenants in multi-tenant applications. If you have thousands of customers and you need to push an update for your solution, you have to do that only for one database and one website, and all customers will get the update. The following diagram describes the multi-tenant architecture:

Multi-Tenant Software Architecture
Multi-Tenant Software Architecture

Design the Database

If we are going to put the data of all customers in the same database, we have to somehow be able to tell which records belong to which customers. There are different ways to do that. For example, we can create a new database schema for each customer. We can use the customer’s name for each schema, so if “Nice Printing” and “Best Insurance” are our customers, all the tables of the “Nice Printing” customer can be placed in the “nice_printing” schema, and all the tables of the “Best Insurance” customer can be placed in the “best_insurance” schema. This will work as long as the number of customers is small and manageable. If there are going to be thousands of customers, this approach can cause maintenance problems.

Another approach is putting data of all customers in the same tables, but assigning each row a unique customer ID that will tell which customer each row belongs to. Then if we want to query data of a specific customer, we can filter it by the customer ID. This approach will be easier to maintain because we can put data of thousands of customers in the same set of tables, and if we have to make updates to our database design, we don’t have to do them separately for each customer. This is the database design that we are going to use in this article.

First, let’s create a table called Tenants. We are going to store the names of all customers in that table. We will name that table Tenants instead of Customers to make it clear that this table is where we are going to store the information about tenants of our multi-tenant application. The following is the Entity Framework model for the Tenants table:

The Entity Framework model class for the Tenants table
The Entity Framework model class for the Tenants table

The Id column is going to be the primary key of the Tenants table. The Name column is going to have the name of the customer. The Domain column is going to have the domain of that tenant. Every tenant is going to have their own domain in our application. For example, if the tenant’s name is Best Insurance, they are going to use the https://best-insurance.com URL to access their site. If the tenant’s name is Nice Printing, they are going to use the https://nice-printing.com URL to access their site. The Domain column of the Tenants table is going to have the “best-insurance.com” and “nice-printing.com” values for each of those tenants respectively. The Domain column will have a unique index on it because we are going to locate each tenant by their domain.

All our tables are going to have a column called TenantId which will tell what rows belong to what tenants. The primary keys of those tables are going to be composite keys consisted of the TenantId and Id columns. The Id column is going to be an Identity column which will auto-increment with every row we add in the database. The TenantId column, aside from being part of the primary key, is also going to be a foreign key referencing the Tenants table. Since all tables are going to have the TenantId and Id columns, it makes a lot of sense to create a base abstract class for them:

The base TenantModel class that all table EF model classes will inherit from
The base TenantModel class that all table EF model classes will inherit from

Next, we have to tell Entity Framework that we want to make the primary key of all model classes inherited from the TenantModel class a composite key consisted of the TenantId and Id column and make the Id an Identity column that auto-increments whenever we add a new row. For that, we have to override the OnModelCreating method of our DbContext class and use the following code:

Entity Framework configuration for all classes inheriting from TenantModel
Entity Framework configuration for all classes inheriting from TenantModel

Now, we can add a table that will actually use the TenantModel class. Our new table is going to be called Products. The Products table will have data from all tenants but the data will be segregated by the TenantId column. The following is the code of the Product model for the Products table:

The Entity Framework model class for the Products table
The Entity Framework model class for the Products table

The Product class is pretty simple. It inherits from the TenantModel class and adds the Name and Description properties to it. The code we added to the AppDbContext class will automatically make the primary key of the Products table composite key consisted of the TenantId and Id columns inherited from the TenantModel class and it will also make the TenantId column a foreign key referencing the Tenants table. Every time we create a new class that inherits from TenantModel, our code will ensure it has a correct primary key for our multi-tenant application.

Segregate Tenant Data

We wrote code that will add a TenantId column to all tables in our database. Next, we have to write code that will actually set the TenantId column value when adding or updating rows in those tables. Every time we are about to add or update data in the database, we have to do that with the correct tenant ID which raises the question how we are going to determine what tenant ID to use for each request in our website. We can extract the domain from the website URL and use that for querying the Tenants table by the Domain column. When we find a Tenants table row that matches the domain in question, we can use the ID of that row. Let’s create an ASP.NET Core filter that will do what we need.

ASP.NET Core filter that sets TenantId on every request
ASP.NET Core filter that sets TenantId on every request

The TenantProviderService.TenantId property that we are setting in the image above on line 42 is a simple property with a getter and setter. We can just register the TenantProviderService class as a Scoped service which means that once the TenantId property is set in our TenantFilter class, it will be available both when serving the HTTP request and response. In the following image, we are globally registering the TenantFilter ASP.NET Core filter to set the TenantId property on the TenantProviderService class for every request and registering the TenantProviderService class as a Scoped service to keep the TenantId property for serving both the HTTP request and response once we set it in the TenantFilter class.

Service registrations for the TenantFilter ASP.NET Core filter and TenantProviderService class
Service registrations for the TenantFilter ASP.NET Core filter and TenantProviderService class

The TenantProviderService.TenantId property will now have the correct tenant ID for each request. Now, we have to use that tenant ID when adding, editing, and deleting records in the database. We can do that by overriding the SaveChanges and SaveChangesAsync Entity Framework DbContext methods and setting the TenantId property of the modified entities to the value of the TenantProviderService.TenantId property.

Setting the TenantId of the records we are about to add, edit, or delete
Setting the TenantId of the records we are about to add, edit, or delete

In the SetTenantId method of the image above, we are iterating through all entities in EF change tracker that inherit from the TenantModel class and setting the TenantId value of those entities to the value of TenantId of the TenantProviderService service. Then we are calling the SetTenantId method from the overridden SaveChanges and SaveChangesAsync methods.

The last thing we have to do is to filter all the data by the current tenant ID when we are reading data from the database. Every time a user needs to see some data, we have to only show the records that belong to the tenant (customer) whose website they are browsing. We can do that by adding a global Entity Framework Core filter.

Global Entity Framework filter for showing only tenant specific data
Global Entity Framework filter for showing only tenant specific data

In the image above, we are calling the new FilterByTenantId method from the SetupTenantModels method. On line 24, we are defining a filter expression that will keep only the rows where TenantId column equals to the value of the TenantProviderService.TenantId column. On line 31, we are registering that filter expression for each table that is created from a class inherited from the TenantModel base class. The code in line 26, 27, and 29 is just for adjusting the filter expression the way that Entity Framework likes it.

Conclusion

If you are developing a SaaS solution, it makes a lot of sense to use the multi-tenant architecture because it makes the maintenance and adding new features really easy for new and existing clients. There are different ways to implement this architecture. The most common way and the one we used in this article is using one database and one website for all tenants (customers). To implement the multi-tenant architecture with the one database and one website for all tenants approach, you have to do the following:

1. Configure the primary key of all tables that will have tenant data to be a composite key consisted of the TenantId and Id columns. The Id column should be an identity column that increments every time we add a new row. The TenantId column should also be a foreign key referencing the Tenants table.

2. Determine the TenantId for each request. In an ASP.NET Core application, we can create a global filter that will get the tenant ID from our database based on the domain of the request URL.

3. When adding, editing, and deleting data, set the TenantId column value to the tenant ID value we got from step 2.

4. When reading data from the database, filter the data by the tenant ID value that we got from step 2.

Please note that the TenantModel base class we used in this article will not work for the many-to-many database table relationships. We didn’t cover configuring the TenantId column for that type of relationships in this article for keeping it short and easy to understand. However, if you are interested in configuring the TenantId column for the many-to-many relationships, please consider reading our article about many-to-many relationships in Entity Framework Core. It doesn’t cover the multi-tenancy, but it can be a good starting point.

https://developerpartners.com/blog/f/many-to-many-relationships-in-entity-framework-core

Author:

Hayk Shirinyan

LinkedIn: https://www.linkedin.com/in/hayk-shirinyan-92980b59/

Website: https://developerpartners.com/

--

--

Developer Partners

🚀 Dynamic software dev company empowering businesses & individuals. Stay tuned for insights, trends, & success stories as we explore software's possibilities.