Software-as-a-Service (SaaS) like JetBrains Space, YouTrack, and TeamCity Cloud are built around the concept of multi-tenancy. Many other services you are using every day probably are, too! Instead of spinning up a dedicated server for every customer, these services often share server resources while keeping configuration, data and user accounts for that service separate and isolated. Much like time-sharing a vacation rental, all tenants can use the swimming pool, but they have their separate set of towels.
In this post, we will explore what multi-tenancy means, why you may consider it when building an application, and how to implement it with the tools we’re all familiar with: Entity Framework Core and ASP.NET Core.
What is multi-tenancy?
At its core, multi-tenancy is an architecture where one codebase serves multiple customers while maintaining data isolation. To customers, it feels like they have their own copy of the software running, while the application really is just one deployment. The focus of multi-tenancy can vary from security, maintainability, reduced infrastructural complexity, and improved resource utilization.
Tenant is a catch-all term, and teams define what it means to them. A tenant can be a user, an organization, or other logical groupings. Although with many SaaS out there, you’ll see an organization is usually the boundary for specifying a tenant.
Teams wanting to adopt multi-tenancy typically have to design applications with the concept upfront. While you can bring multi-tenancy into existing applications, it makes it easier to build solutions when multi-tenancy is considered foundational. Multi-tenancy typically touches every aspect of an application from authentication and authorization, business logic, database schema, and isolation, and sometimes even elements users won’t see like the hosting environment.
The ultimate goal of multi-tenancy is to significantly reduce an area of complexity, with the trade-off being some additional complexity. So first, let’s look at some examples of multi-tenancy approaches and their advantages and disadvantages. We’ll be implementing these later with Entity Framework Core and ASP.NET Core.
Multiple Tenants, Same Infrastructure
The most common approach to multi-tenancy is to group all tenants into a single instance, with mechanisms to separate all groups. The separation mechanism is typically logical, with code paths understanding what group is making a request and applying the necessary filters to reduce access to only relevant information.
Advantages to this approach are plenty: you can share infrastructure, a single codebase, and ultimately one understanding across a team. One deployment updates all tenants to a newer version. Finally, running the approach can reduce hosting costs, eliminating the need for additional hosting servers and dependencies, saving a business’ operational budget.
The most notable disadvantage of this approach is the overhead of managing logical barriers between tenants. In addition, the method can add complexity to the codebase, require more data modeling, and ultimately sharing resources can exhaust allocated infrastructure more quickly. An ill-designed feature can jeopardize your entire user base and trust in your application.
Multiple Tenants, Different Infrastructure
Instead of isolating tenants logically, you may want to consider separating them physically. The approach is where you have different infrastructural dependencies based on the logical group accessing the application.
It becomes almost impossible for one tenant to access the resources of another tenant. You have different servers for all dependencies, but typically most developers will separate storage for each tenant. The physical separation can reduce the cognitive complexity of accessing data, as there is no need for additional filters in code. Finally, significantly smaller instances of databases can also visibly impact performance, with no other tenants using up valuable resources.
The challenge to this approach is your team managing different variants of the same application. In times of crisis, isolating which tenant may or not be in trouble can be challenging. In addition, each tenant is a replica, costing you multiples of what it may cost to host a single instance. Another factor to consider is the chance for feature drift among tenants. One customer asks for a unique feature, then another, and one more. Before you know it, you are managing multiple codebases.
Both approaches can also be combined: a single infrastructure for all tenants that runs the application, with isolated infrastructure such as dedicated storage and a separate database per tenant. While you may be wondering which approach is best, the “best” depends on your situation with considerations of risk, compliance with local regulations, and practicality. Consider your context, then make an educated decision.
Now that you have a basic understanding of multi-tenancy let’s look at some ASP.NET Core and EF Core examples that explore both options.
Single Database Multi-tenancy with EF Core
At the end of this section, we aim to have an endpoint in an ASP.NET Core application that retrieves a tenant’s data. To build a complete example, we need a mechanism that understands who the user is, which tenant they are attempting to access, and one more element that filters the data. You can change these three parts based on your use case, but for the sake of simplicity, we will be using the following techniques:
- A query string parameter of
tenant
set by the user - An ASP.NET Core middleware that reads and sets the tenant based on the parameter
- An EF Core DbContext that uses the tenant to filter queries
Let’s start with the database. Like most .NET applications powered by Entity Framework Core, you’ll have at least one DbContext
. In our case, we have a Database
class that defines our database model and configuration.
There are two essential items in the DbContext
code. First, it’s the constructor of our Database
class, which takes two arguments.
There’s the DbOptions
instance, which allows us to set our database options. The creation of this instance comes from our services collection, typically defined at the start of the program. We’ll see this later in the post. There’s also our ITenantGetter
service, which informs us which tenant we are operating within. We’ll use these parameters in the next method, OnModelCreating
.
During the instantiation of Database
, our service locator will invoke the OnModelCreating
, allowing us to change the tenant and apply the correct value to HasQueryFilter
. Applying HasQueryFilter
adds an implicit filter to all queries that use the entity of Animal
, which means we have to worry about one less thing as we write our application. It’s important to apply such a query filter to any tenant-focused entities and their associated DbSet
property to ensure proper data isolation. So, how do we get the tenant value?
As we move to our Program
class, we’ll see registrations for our implementations. Note that you can find more of this code in the sample repository at the end of this article.
We register the TenantService
and the two interfaces of ITenantGetter
and ITenantSetter
. I chose to separate the interfaces for more apparent intent, but you could just as easily have a single service. So what does the TenantService
do?
The service doesn’t do any more than holding the tenant value for the lifetime of the user request, which allows other objects to understand the scope of their work. In our case, the Database
will receive the tenant value when our service locator creates it. So, what uses the TenantService
? The MultiTenantServiceMiddleware
of course.
The middleware currently tries to find a tenant
query parameter and sets the value using the ITenantSetter
instance. For the purposes of this post, the query parameter gives us the ability to experiment more easily transitioning between tenants. You’d likely read tenant information from a secure and encrypted authentication cookie in a production application to avoid user tampering. Or use the current hostname to distinguish between tenants.
When no tenant is found, the middleware uses a standard tenant. Instead of doing this, you could also implement a redirect to the marketing website where users can sign up to create a new tenant on your service.
Finally, let’s complete the contents of our Program.cs
file.
Great! Now, as you start to make requests to the root endpoint, we’ll either fall back to the default tenant of Internet
or can move to the Khalid
tenant.
We just completed logical multi-tenancy, but what about physical multi-tenancy? Well, we’ll explore that in the next section.
Isolated Databases Multi-tenancy with EF Core
When it comes to moving tenants into different databases, it requires a few changes to the application seen above. Some of the changes required include:
- Moving tenants to configuration along with connection strings
- Altering the middleware to find the tenant from configuration
- Modifying the
TenantService
to use an object rather than key
Let’s first start with the configuration found in our sample project. We are using applicationSettings.json
to define our tenants, but a production application may choose to represent tenants during the deployment process or the infrastructure building phase of development. Therefore, the source of tenant information is not as important as the information itself. After looking at the code, decide where you’d prefer to store and manage it for your use case.
Notice we have two items in our Tenants
collection, each with its unique database connection string. We’ll be using this information in our updated MultiTenantServiceMiddleware
class. Let’s see how.
The new implementation differs from the previous implementation by dealing with a Tenant
object rather than a string
identifier. Our object contains the name and the connection string to our database. To take advantage of our new connection string, we need to modify our DbContextOptions
. We can do that back in our Program.cs
file. We have to include a default connection string to continue to generate migrations, but that will be unused while our application is running.
I’ll include the entire file to clarify where to place the updates in your existing application.
This approach works because every time we make a request, our Database
instance is created based on the information within the user request, allowing us to swap the connection string.
Conclusion
In this post, we saw two ways in which you can use Entity Framework Core and ASP.NET Core to build multi-tenant applications. One approach uses delimiters and query filters to limit the data users see, while the other puts user data into separate physical storage.
Both approaches have their advantages and disadvantages, so you’ll have to pick the one that works best for you. You’ll also want to consider alternative differentiation mechanisms for switching between tenants, have it be auth, domain names, or other important factors in your application.