Multi Tenancy

Multi-Tenancy Architecture with .NET

The multi-tenancy architecture is a common approach to building SaaS solutions, enabling the minimization of resources and maximizing utilization. It's like sharing an apartment building. One building (software) hosts many tenants (users or groups). Everyone gets their own space but shares the main structure. Think SaaS (Software as a Service) – like Netflix or Spotify. One system, tons of users, all getting a personalized experience. It's the opposite of everyone having their own house (separate software instances).

This document is a comprehensive guide to understanding multi-tenancy architecture, its challenges, advantages, database deployment scenarios, identifying the active tenant, isolating data, conditionally turning on/off multi-tenancy, handling migrations and lastly, the requirement for multi-tenancy for your application.

What is Multi-Tenancy Architecture?

Parties
  • Tenants: Our clients using the service
  • Host: Service provider

It is a software architecture in which a single instance of software runs on a server and serves multiple tenants. In this scenario, hardware and software resources are shared (rather than "dedicated" or "isolated"). A tenant is a group of users who share a common access with specific privileges to the platform. With a multitenant architecture, it is designed to provide every tenant with a dedicated share of the instance, including its data, configuration, user management, tenant-specific functionality, and non-functional properties. Multitenancy contrasts with multi-instance architectures, where separate software instances operate on behalf of different tenants.

An Ideal Multi-Tenant App Should Be

  • Unaware of multi-tenancy as much as possible! Multi-tenancy should be designed to work seamlessly under the hood.
  • Technically separated from multi-tenancy: Do all your tenancy-related stuff in a low-level layer and keep your business code clean as much as possible. You should not pass TenantId to all your controllers, application services, repositories or domain services. By doing so, developers will not forget to include multi-tenancy-related code, and it will not be open to developer mistakes.
  • Deployable to on-premise: When a customer wants to set up your solution on their own servers, you should be doing that without any code changes.

Multi-Tenant Apps vs On-Premise Apps

In the following table, you can see the difference between the on-premise (single tenant architecture) and multi-tenant architecture:

On-Premise Architecture Multi-Tenant Architecture
Data Isolation Physically isolated! No need to implement a special isolation. Data isolation must be done on the apps, database, and other resources.
Customization Every customer has their own code base. Customize how they want. Customization is limited! You must provide ways to change the UI, configs, and behaviours.
Scalability Scalability depends on the customers’ resources. Not easy to upgrade when needed. Highly scalable; new tenants can be added with just a new database record.
Security Higher security because data is isolated physically. Security risks due to shared resources.
Cost Higher costs due to running dedicated resources. Lower costs because customers share the resources & expenses.
Maintenance Maintenance must be performed separately for each tenant. Central updates and upgrades are applied to all customers simultaneously.
Performance Performance only changes if the customer consumes more. Performance can change if other tenants consume resources heavily.

As-a-Service Business Models

In multi-tenancy, there are four main models in software development & deployment. These are on-premise, infrastructure as a service, platform as a service, and software as a service.

On Premises

Or sometimes called "on-prem", is installed and runs on computers on the premises of the organization, using the software, rather than at a remote facility. On this model, the customer pays and manages for the complete software and hardware resources. This is the most expensive way of using software. But on the other hand, if the customer has custom tasks, security concerns, or government regulations, then this model is a must.

Platform as a Service (PaaS)

In this model, customers manage only their software and database. They can develop, run, and manage web applications without having to worry about the underlying infrastructure. PaaS provides customers with a platform to build and deploy applications quickly and easily.

Infrastructure as a Service (IaaS)

Or sometimes called “dedicated server”. In this model, customers have access to computing resources, like servers, storage, and networking. It allows users to rent these resources on a pay-as-you-go basis, so they don't need to invest in hardware or software upfront. IaaS customers have full control over the virtual machines and can install and configure their own software and applications. These are examples of IaaS: Google Compute Engine, Azure Virtual Machines.

Software as a Service (SaaS)

It is a cloud computing model in which software applications are hosted on a remote server, and customers access them through a web browser / mobile apps. SaaS eliminates the need for customers to install and manage the software on their own systems. This model maximizes the utilization and minimizes the costs.

Advantages

Cheaper

You share the hardware and software among customers, you reduce costs and serve the maximum number of customers.

Consistent User Experience

All our customers use the latest version.  So we, as developers, can focus on maintaining a single codebase, ensuring that all tenants receive updates and improvements at the same time.

Easy Updates

Maintaining a single codebase and infrastructure for all tenants simplifies software updates, patches, and bug fixes. It reduces the complexity of managing multiple instances, making it easier for developers and administrators to maintain the system.

Scalable

When there are demand spikes, you can easily increase system resources. You can add extra servers behind your load balancer. This way, you can serve more customers.. This leads to better resource utilization and responsiveness to demand spikes. But if it were an on-premises system, then it would be hard to increase the resources of each tenant.

Faster Onboarding

New tenants can be onboarded quickly within the existing infrastructure; you don’t need to set up a new environment for the new client. When a new tenant comes, you just add a new line to your Tenants table.

Challenges

Data Isolation

Gotta keep everyone's stuff separate! No peeking into other people's apartments.Ensuring proper data isolation btw tenants to prevent unauthorized access to sensitive information.

Customization and Configuration

Everyone wants to paint their walls a different color. Need to allow customization without breaking the whole building. Your clients request to customize the application according to their requirements. They want to rebrand and customize the UI, logo, and colors. Managing different configurations and customizations for each tenant without compromising the core architecture can be challenging.

Performance Balance

Some tenants might use a TON of resources and slow things down for everyone else. Customers who use the system extensively are called “Noisy neighbors“. We should ensure that the resource usage of one tenant does not negatively impact the performance of the other. This should be done by monitoring the system.

Security

When a hacker gets into your server, they can steal all your client data. Also, if you have a security hole, a tenant can gain access to another tenant’s data.

Backup and Recovery

This involves database and storage backup per tenant. It will be very easy to backup/restore when you have a separate DB for each tenant, but if you have a shared DB, then you need to get a backup of the specific tenant. And tenants may have different retention policies, so you need to implement different strategies for each tenant. Government agencies + banks.

Deployment & Database Architectures

There are four scenarios for the application and database deployments.

1. Separate Application & Separate Database

Like everyone having their own house. Super isolated, but expensive and hard to manage. In this model, each tenant has their own application and database instance. This one is on-premises deployment. It is not a SaaS friendly architecture.

2. Shared Application & Separate Database

Everyone shares the building but has their own storage unit. This is better than the first one. All the clients share the same application but use separate DBs. Not good for resource utilization. Because you need to maintain/update the schemas of the databases.

3. Separate Application & Shared Database

Everyone shares everything! This one is the ideal one. Saas friendly! Everyone uses the same app and the same DB—minimum cost with maximum client coverage. The downside of this approach is that some customers might have excessive data and consume resources much more than others. Also, according to some GDPR rules, some clients may want to locate the DB in their country, like banks. Therefore, you need to separate those DBs.

4. Hybrid

Mix and match! The last one covers all kinds of challenges. You can provide a separate DB if a client pays more or locates their data in a different geo-location. On the other hand, small clients can share the same DB.

Maintaining Application States

A web application state defines user-specific data. The web apps are hosted on the HTTP protocol. And HTTP is a stateless protocol. This means the user data is not persisted from one web page to the next. So, a stateless web application does not save any client session (state) data on the server. From the multi-tenancy perspective, the TenantId should not be stored on the server.

A multi-tenant web application should be stateless!

Besides, the stateless web approach is also multi-thread friendly!
Then there is this question: “Where should we save the state?”

Where should we save the state?🤔

  • HTTP Request (cookie, header, query string, payload, etc)
  • Authentication ticket
  • Database
  • Distributed cache (Redis, Memcached, ...)

We can save the state in HTTP request parts, in the authentication tickets as in JWT format, or we can save it in the database, or in distributed caches like Redis or Memcached.

Identifying the Active Tenant

In a multi-tenant application, the users belong to an organization, which is called the tenant. So when a user logs in or after logging in, he makes subsequent requests, we need to identify which tenant’s user this user is. There are several ways to understand the current/active tenant. In ABP, we use six ways to identify the active tenant. This list is sorted by priority of use. Each subsequent tenant resolver acts as a fallback to the previous one. (In the ABP Framework, we call each tenant identification class a TenantResolveContributor).

  • 1. CurrentUserTenantResolveContributor
  • 2. QueryStringTenantResolveContributor
  • 3. RouteTenantResolveContributor
  • 4. HeaderTenantResolveContributor
  • 5. CookieTenantResolveContributor
  • 6. DomainTenantResolver

In the next section, we will explore the basic implementation of these contributors. The following subsections include simplified code snippets from the open-source ABP Framework, designed for easier understanding.

1. Identifying Active Tenant: Current User (Claims)

In the first method, we use claims to save the TenantId. When a user logs in, ABP Framework saves the TenantId to the claims. And when the user comes back again, we identify the user from this TenantId that’s retrieved from the claims.

2. Identifying Active Tenant: Query String

This is the second way and is the fallback of the first way. Here, the tenant is being retrieved from the query string. We basically use the HttpContext of the ASP.NET Core framework. You can store the TenantId in the URL as the primary key or the name of the tenant, or you can give a special code to each of your tenants. You can see the real implementation of this way in ABP Framework ➡ QueryStringTenantResolveContributor.cs.

3. Identifying Active Tenant: Route

The method retrieves the Tenant ID or Tenant Code from route values. After the user logs in, you can store the tenant name or code as a URL segment (like "acme" in the example below). The tenant can be obtained easily using HttpContext.GetRouteValue().
You can see the real implementation of this way in ABP Framework ➡ RouteTenantResolveContributor.cs.

4. Identifying Active Tenant: Header

In this method, the tenant information is stored in the request headers. As you can see, in the example below, the tenantId is a GUID and is being sent with the name `__tenant` in the request header. We use HttpContext.Request.Headers collection to get the value. If this value is empty, we continue with the 5th method. This method is commonly used in single-page applications or mobile apps.
You can see the real implementation of this way in ABP Framework ➡ HeaderTenantResolveContributor.cs.

5. Identifying Active Tenant: Cookie

Like the previous methods, in this way, tenant information is stored in cookies, similar to using request headers. Cookies are ideal for browser-managed scenarios, particularly when tenant data should expire after the session ends or at a set time interval. They are also convenient for backend communication, as browsers automatically include them in requests.
You can see the real implementation of this way in ABP Framework ➡ CookieTenantResolveContributor.cs.

6. Identifying Active Tenant: Domain

Holding tenant information in the domain name is a good practice. In this way, we can provide a subdomain to each of our tenants. For example, for an e-commerce website, we can give Adidas the tenant adidas.myshop.com or Nike the tenant nike.myshop.com so that each tenant will have a unique domain, and there will be no need to ask a second tenant for information on the login screen. We can retrieve the subdomain name of a request easily with the help of Request.Host.Value of the ASP.NET Core. In the below example, we did not show the implementation of the Parse method, but you can simply implement it via RegEx.
You can see the real implementation of this way in ABP Framework ➡ DomainTenantResolveContributor.cs.

Data Isolation

Data isolation is the backbone of the multi-tenancy topic. Basically, we can isolate the tenants’ data in two ways: physically or virtually. The physical isolation is simple; you just need to locate each tenant’s database separately, and there will be no conflict about isolating their data. But when it comes to sharing a single database with your tenants, you will store all tenants’ records in the same tables. This means we need to soft-isolate them. In the following example, you see a traditional way of isolating tenants’ data:

This approach requires manually adding a TenantId filter to every database query, which is not very practical. Junior developers or new teammates might forget to include this filter in new repository methods. Moreover, this infrastructure concern clutters the query logic. We should take this filtering in a low-level framework to keep our code clean and neat. In the ABP Framework, we use an interface called IMultiTenant, seen in the following image.

The IMultiTenant interface adds a single property, TenantId <Guid>. By using this interface, we standardize the TenantId field and ensure that a multi-tenant entity will always have a TenantId property. And this helps us to filter records programmatically.

ABP Framework supports;

  • Shared Database: All tenants are stored in a single database.
  • Database per Tenant: Every tenant has a separate, dedicated database to store the data related to that tenant.
  • Hybrid: Some tenants share a single database, while some tenants may have their own databases.

And in the following section, we will see how the multi-tenancy feature works seamlessly at the framework level.

If you are using EF Core as your database management system, there is some good news! EF Core has a feature called Global Query Filters. This feature allows you to define a filter condition that is automatically applied to all queries for a given entity. So you can use this feature for multi-tenancy, filtering soft-deleted records or any custom field that you define, like Book.IsPublished field.

These filters are LINQ expressions that apply to entities in the OnModelCreating phase. ABP Framework uses this system for the EF Core Integration. So, it is well integrated with EF Core and works as expected even if you directly work with DbContext.

Data Isolation — EF Core Manual Way

When you want to implement the EF Core’s Global Query Filters, you simply use HasQueryFilter for all your multi-tenant entities in the OnModelCreating method of the DbContext.
As you see in the following example, we add a tenancy filter to the Book entity in our DbContext. But you need to add this filter for all your entities manually. This is open to human mistake (developers can forget to add this filter). Let's see how ABP Framework automates this process.

Data Isolation: Implementing EF Core Tenant Filters — the ABP Way

ABP Framework finds all entities that implement an IMultiTenant interface. This is being done with the help of reflection (IsAssignableFrom). Secondly, ABP creates a LINQ expression that filters the tenant with the active tenant’s ID. And lastly, this filter is being added to the query filters with the HasQueryFilter method. Hence, all the IMultiTenant entities are automatically added to the EF Core’s Global Query system. Developers do not need to write any extra logic to make an entity multi-tenant.

Data Isolation — EF Core PROS & CONS

There are some advantages and disadvantages of this solution. First of all, this solution works on Global Query Filters, and you are tightly coupled to EF Core. This can be a disadvantage for your solution if you want to replace EF Core with another ORM in the future. On the other hand, it is very easy to implement, and it even supports navigation properties.

Another side-effect is that sometimes you need to disable the multi-tenancy to get a cumulative report over all tenants or get the soft-deleted records. For these use-cases, EF Core suggests using the IgnoreQueryFilters method. This is good, but it disables all the filters for a specific LINQ query. You can't just disable multitenancy but leave the soft delete filter active.

Also, there is another limitation of IgnoreQueryFilters… Filters can only be defined for the root Entity Type of an inheritance hierarchy.

In this example, Animal is the root entity type. BigAnimal and SmallAnimal inherit from Animal. You can just define global query filtering for the Animal class.

Developers who use Stored Procedure or raw T-SQL queries will not like using Global Query Filters because the filtering does not include these. So if you use the below queries, you must add the TenantId filter manually. Or there is another feature called Row Level Security.

Data Isolation — SQL Server Row Level Security

Row Level Security covers almost all cases, which were explained in the previous sections. Row-Level Security (RLS) is tied to the database user or session context, not directly to the application or connection string. RLS evaluates the current database user’s identity when applying the filter. This user is the one that’s connected to the database, which may be a real login or a mapped user in contained databases. While RLS seems like a perfect solution, it is relatively complex to implement, and you need to stick to SQL Server. That’s why ABP Framework uses EF Core Global filters method as it is ideal for a pragmatic solution.

Data Isolation — MongoDB

Data isolation on relational databases is explained above. But if you are using a non-relational database like MongoDB, what is the solution for multi-tenancy? Well, there is no built-in feature for global filtering; that is why ABP implements a custom solution for MongoDB.

ABP abstracts the IMongoDbRepositoryFilterer interface to implement data filtering for the MongoDB Integration. The full implementation can be found at MongoDbRepositoryFilterer.cs. In this solution, ABP filters all entities that implement the IMultiTenant interface, then creates a filter for the active tenant and finally, adds it to a global filter. This solution works if only the repository methods get called.

Setting TenantId for New Entities Automatically

In a multi-tenant application, each time a new entity is created, you need to set the tenantId according to the active tenant. But when you do this manually, it can be forgotten. To avoid mistakes, in the ABP Framework, we set the TenantId for a new multi-tenant entity in the constructor. In the below example, ABP gets the TenantId from the active tenant’s scope and sets it. This way, we are sure that TenantId is always being set.

DB Connection String Selection

When your application allows customers to choose their own database, you need to save each tenant’s DB connection string. You will have a master database and child tenant databases. The master database connection string can be stored in the configuration file (appsettings.json). And if a tenant wants a separate database, then store its connection string in a separate database table in the master database. For this purpose, ABP has the `AbpTenantConnectionStrings` table as seen below. This table just stores the TenantId and the connection string value.
This way, we can set up a hybrid approach for both shared and dedicated database architecture.

Here’s the implementation:
We use a factory service to dynamically set the connection string when the DbContext is being created. We use DbContextCreationOptions for this approach. For more information, see

https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontextoptionsbuilder.

Changing the Active Tenant

So far, we have learnt the fundamental features of multi-tenancy. In each HTTP request, you can run queries only for the active tenant.
But sometimes, you may need to change the active tenant. For example, when you have a background job that generates reports for each tenant (or in Windows Services). In this case, there will be no active tenant. Or when you want to run a console app without a web context, you need to set the active tenant manually. For this purpose, ABP Framework uses a disposable method to switch/set tenants.

We add a Change method to the CurrentTenant object. This method is being used within a using block so that it will revert back to the original tenant after the code inside executes. The implementation of the Change method is simple. Here we keep the original tenant in a temporary variable and set the new tenant. Doing this, we filter all queries by this tenant.

Then we restore the original tenant after using the existing statement. So all the logic that checks the current tenant will read this new value.

And we created a middleware called MultiTenancyMiddleware. This middleware sets the current active tenant. And we add this middleware before the authorization middleware.

Temporarily Disable Multi-Tenancy

Sometimes, you may need to query all tenants, especially when your tenants share the same database. For example, you may want to get reports from your tenants. In this example, we get all book counts without the tenancy filter. Within the _filter. Disabling the tenancy filter will not work. After the “using” block finishes, TenantId will be restored, and multi-tenancy filtering will be restored again.

In the image below, you can see the implementation of the Disable and Enable methods. We use a singleton dependency for this class. We store all the filters in the concurrent dictionary. The Disable method just sets the IsEnabled flag of the filter object. And each time we add a filter to the EF Core Global Query Filters, we check this boolean flag.

Database Migration

Database migration in a multi-tenant architecture can be overwhelming because there can be hundreds of standalone tenant databases. There can be many ways of achieving this, but we will discuss the two main approaches. The first one is creating a DB migration tool, and whenever we update our application’s database, we will update all the customers’ databases at the same time. Even though this is easy to implement, the customers can wait a long time, especially when their database is really big. And you cannot run the application until all the tenants’ databases are updated because the schema might be changed, and your application will not work properly if you don’t update all the databases.

Approach-1: Make a DB migration with a custom tool

There’s a second approach. You start a tenant’s database when the tenant interacts with the database for the first time. It means that unless they use the system, their database will not be updated. We do this because we want to spread the upgrade process over a longer period of time. But there is another handicap! The first user who makes the first request can wait a long time as he initiated the database update process. And even the request might time out! Also, you need to handle the concurrency issues if several users from the same tenant start the DB update process.

Approach-2: Run migration on the first DB access

Let’s take another approach. In the 3rd approach, we set up two different application servers. These contain the old version and the new version of our application instances. You will put a loadbalancer in front of the server. This loadbalancer will understand which user the incoming request is from, and if the database of the relevant user has been updated, it will direct it to the latest server; if not, it will direct it to the old server. By doing this, you will update your servers and databases in the background. When the update is complete, you will send an email to the user to let them know that they have switched to the new version. With this method, no one will be victimized, and the transition will be seamless. The only disadvantage of this method is that you have to put a load balancer in place and run two different versions at the same time. Also, users who switch from the old version to the new version should not lose data!

Approach-3: Make two versions of the application servers

Feature Management

When you have a multi-tenant SaaS application, the feature system is also part of this architecture. To explain what a feature means, first, we need to know the term: Edition. A standard SaaS application sells the customers different plans with different prices. Each subscription plan is called an edition. And each edition should consist of different features. Netflix is a good SaaS example. It has three editions and eight features. We need to enable/disable each feature based on the current tenant at runtime.

In the ABP Framework, there is a built-in feature management system, see ABP Feature System. In this system, ABP stores all the features in a read-only dictionary.

We created the [RequiresFeature] attribute to be used on methods. When you add [RequiresFeature], the ABP framework intercepts the method and injects a check before the execution of the method. This is the RequiresFeatureAttribute… So, it works with any class that is injected through dependency injection. The check is done at FeatureCheckerBase.cs. Also, ABP allows for inline checking of the feature.

Do You Need Multi-Tenancy?

So far, almost all the major subtopics of the multitenancy development have been discussed in this document. But keep in mind that multi-tenant development is hard! Many developers start a multi-tenant architecture even though it is not needed. Make sure you actually need it before diving in.
If you want to understand if you really need multi-tenancy, you can read this article 👉 Do You Need Multitenancy?


What Customers Say About the ABP Platform?

"The ABP Framework has been a transformative tool for our development process. Its unique features, like modularity and built-in multi-tenancy, have significantly streamlined our projects. The ability to choose from different UI frameworks and databases allowed us to customize our applications to meet specific client needs without compromising efficiency. The comprehensive documentation and active community support have also been invaluable resources. Overall, the ABP Framework has enabled us to deliver high-quality software solutions faster and more effectively than ever before."

Logo
Phillip Schulte
P&M Agentur

Get Started with Your Modular Application Today

Use the following resources to start building your modular monolithic application today using the ABP Platform:

External Resources and Further Reading

Use the following resources to start building your modular monolithic application today using the ABP Platform: