Build Multi-Tenant Applications with EF Core

bART Solutions
5 min readJul 17, 2023

--

Plenty of modern software solutions are designed with the concept of multi-tenancy in mind.🤯Instead of scaling up a separate application example for each client, a multi-tenant approach allows a single application to serve multiple clients while maintaining the isolation of their data.
There are 2️⃣ main approaches to implementing multi-tenancy, and each of them has its pros and cons. The choice between them depends on the specific requirements and resources of your organization.
🟪 Single database with logical isolation of tenants. This approach assumes a common database for all tenants, but their data is logically separated. For instance, you can add the field “Tenant” to all tables and filter all data based on this value during queries. The benefits of this approach are the simplicity of implementation and the absence of the need for additional dependencies and hosting servers.😋 The main drawback is that the sharing resources can “exhaust” the dedicated infrastructure much faster and increase the size of the database. Also, because all tenant data is shared, errors in feature development can lead to information leaks, which threaten customer security and your reputation.🤕
🟪 Multiple databases with physical seclusion. The approach, also known as database-per-tenant, involves allocating a separate database for each tenant, which provides physical data isolation. Information about tenants and their connection strings is stored in the configuration, a central database, or special storage (for example, Azure Key Vault or AWS Secrets Manager). The database is determined, when a request is received, based on the current tenant. This approach has several important advantages.1️⃣, it ensures data isolation, as information of each tenant is stored separately and is not available to other tenants. It guarantees privacy and data protection.2️⃣, this approach improves system performance, since each tenant has its instance of the database, the size of each database becomes much smaller. The disadvantages of this approach are increased costs for hosting resources and the complexity of implementation and support.
First, let’s consider the implementation of the common database approach using EF Core Query Filters. All given examples are simplified for better understanding and need improvement for use in a production environment.☀️
To start with, we need to define MultiTenantMiddleware and TenantProvider, whose role is to determine and store a unique tenant identifier (name) from requests from the client.💻For a production environment, you can consider more secure sources of obtaining information about the tenant, for example, authentication cookies, JWT claims, or API keys.

TenantProvider implements two interfaces ITenantSetter and ITenantGetter for greater flexibility. The provider only stores and returns the current value of the tenant during the lifetime of the user’s request.⏳

We register the necessary dependencies in the DI container:

In the configuration class, for each entity, apply a filter on the Tenant field to all requests to the database. This filter can be configured using EF Core’s built-in HasQueryFilter method. Additionally, you can implement a mechanism for the global “setting” of the Tenant value when writing data to the database.🖥

Next, we consider an implementation option with isolated databases and saving information about tenants in the central database.

First, you change the TenantProvider class and the ITenantGetter and ITenantSetter interfaces. In addition to the name, the provider will store the connection string of the tenant, which is obtained from the central database based on the current tenant ID.⌨️🖱

🔍You also need to change the DbContext registration a bit to dynamically “resolve” the connection string based on the current tenant.

If you need more flexibility, for example, to apply migrations from code, then you can use the implementation of the IDbContextFactory interface to register and “resolve” the database context.🔓

In this example, the IApplicationDbContextFactory interface performs the role of a “wrapper” over the factory. It has an additional method CreateDbContext(string connectionString), that allows you to explicitly specify for which tenant the database context should be created.🏳️The logic of applying migrations for all database tenants can look like this:

The last step😎 for the proper operation of design time migrations is the implementation of the IDesignTimeDbContextFactory interface. The factory must create a context used during design time operations, for example, Migration (add-migration Initial –context ApplicationDbContext).

The implementation of multi-tenancy architecture can be difficult, but with the use of appropriate templates and approaches, it is an effective solution for companies and organizations.💪While maintaining a high level of security and operational effectiveness, this strategy enables you to process and store data from numerous clients.☝️

Don’t forget to visit our website to learn more about bART Solutions.

Follow the link: https://bart-solutions.com

--

--