Multi-Tenant SaaS Architecture with Entity Framework
Tags: dotnet, sql, csharp, database
Published: 2023-09-07 23:22:31

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wi0qmti6fg6g6mw3vj94.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/00nymlxsayk9hyksrajb.png)

## 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:
```
[Index(nameof(Tenant.Domain), IsUnique = true)]
public class Tenant
{
    [Key]
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Name { get; set; }

    [Required]
    [StringLength(100)]
    public string Domain { get; set; }
}
```

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:
```
public abstract class TenantModel
{
    public int TenantId { get; set; }
    public Tenant Tenant { get; set; }

    public int Id { get; set; }
}
```

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` columns 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:
```
public class AppDbContext : DbContext
{
    public DbSet<Tenant> Tenants { get; set; }

    private void SetupTenantModels(ModelBuilder modelBuilder)
    {
        var tenantModels = modelBuilder
            .Model
            .GetEntityTypes()
            .Where(e => typeof(TenantModel).IsAssignableFrom(e.ClrType));

        foreach (var model in tenantModels)
        {
            // Set primary key to a composite key consisted of TenantId and Id columns.
            modelBuilder.Entity(model.ClrType)
                .HasKey(nameof(TenantModel.TenantId), nameof(TenantModel.Id));

            // Make the Id column Identity that auto-increments with every row.
            modelBuilder.Entity(model.ClrType)
                .Property(nameof(TenantModel.Id))
                .ValueGeneratedOnAdd();
        }
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        SetupTenantModels(modelBuilder);
    }
}
```

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:
```
public class Product : TenantModel
{
    [Required]
    [StringLength(100)]
    public string Name { get; set; }

    [StringLength(300)]
    public string Description { get; set; }
}
```

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.
```
public class TenantFilter : IActionFilter
{
    private readonly AppDbContext _dbContext;
    private readonly IHostEnvironment _environment;
    private readonly ITenantProviderService _tenantProviderService;

    public TenantFilter(
        AppDbContext dbContext,
        IHostEnvironment environment,
        ITenantProviderService tenantProviderService)
    {
        _dbContext = dbContext;
        _environment = environment;
        _tenantProviderService = tenantProviderService;
    }

    private string GetCallingDomain(HttpRequest request)
    {
        var callingUrl = $"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}";
        var uri = new Uri(callingUrl);

        return _environment.IsDevelopment()
            ? $"{uri.Host}:{uri.Port}"
            : uri.Host;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        var domain = GetCallingDomain(context.HttpContext.Request);

        var tenant = _dbContext
            .Tenants
            .SingleOrDefault(t => t.Domain == domain);

        _tenantProviderService.TenantId = tenant.Id;
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Do nothing here.
    }
}
```

The `TenantProviderService.TenantId` property that we are setting in the code snippet above on line 42 is a simple property with a getter and setter. We can just register the T`enantProviderService` 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 code snippet, 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.
```
public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container.
        builder.Services.AddControllersWithViews(options =>
        {
            options.Filters.Add(typeof(TenantFilter));
        });

        builder.Services.AddScoped<ITenantProviderService, TenantProviderService>();

        // Irrelevant code omitted for brevity
    }
}
```

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.
```
public class AppDbContext : DbContext
{
    // Irrelevant code omitted for brevity

    private void SetTenantId()
    {
        foreach (var entry in ChangeTracker.Entries<TenantModel>())
        {
            entry.Property(e => e.TenantId).CurrentValue = _tenantProviderService.TenantId;
        }
    }

    public override int SaveChanges()
    {
        SetTenantId();
        return base.SaveChanges();
    }

    public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        SetTenantId();
        return base.SaveChangesAsync(cancellationToken);
    }
}
```

In the `SetTenantId` method of the code snippet 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.
```
public class AppDbContext : DbContext
{
    private void FilterByTenantId(ModelBuilder modelBuilder, IMutableEntityType model)
    {
        Expression<Func<TenantModel, bool>> filterExpression = t => t.TenantId == _tenantProviderService.TenantId;

        var newParam = Expression.Parameter(model.ClrType);
        var newBody = ReplacingExpressionVisitor.Replace(filterExpression.Parameters.Single(), newParam, filterExpression.Body);

        LambdaExpression lambdaExpression = Expression.Lambda(newBody, newParam);

        modelBuilder.Entity(model.ClrType)
            .HasQueryFilter(lambdaExpression);
    }

    private void SetupTenantModels(ModelBuilder modelBuilder)
    {
        var tenantModels = modelBuilder
            .Model
            .GetEntityTypes()
            .Where(e => typeof(TenantModel).IsAssignableFrom(e.ClrType));

        foreach (var model in tenantModels)
        {
            // Set primary key to a composite key consisted of TenantId and Id columns.
            modelBuilder.Entity(model.ClrType)
                .HasKey(nameof(TenantModel.TenantId), nameof(TenantModel.Id));

            // Make the Id column Identity that auto-increments with every row.
            modelBuilder.Entity(model.ClrType)
                .Property(nameof(TenantModel.Id))
                .ValueGeneratedOnAdd();

            // Globally filter all queries by TenantId
            FilterByTenantId(modelBuilder, model);
        }
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        SetupTenantModels(modelBuilder);
    }
}
```

In the code snippet above, we are calling the new `FilterByTenantId` method from the `SetupTenantModels` method. We are defining a filter expression that will keep only the rows where `TenantId` column equals to the value of the `TenantProviderService.TenantId` column. We are registering that filter expression for each table that is created from a class inherited from the `TenantModel` base class. The code that uses expression trees and expression visitors 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.
[Many-To-Many Relationships in Entity Framework Core](https://developerpartners.com/blog/f/many-to-many-relationships-in-entity-framework-core)