Multi-Tenant Applications With EF Core

Milan Jovanović - Oct 14 '23 - - Dev Community

Most software applications today are built around the concept of multi-tenancy.

One application serves multiple customers, while keeping their data isolated.

You can approach multi-tenancy in two ways:

  • Single database and logical isolation of tenants
  • Multiple databases and physical isolation of tenants

Which option you decide to use will depend mostly on your requirements. Some industries like healthcare require a high degree of data isolation, and using a database per tenant is a must.

So how are we going to implement multi-tenancy support with EF Core?

We can use Query Filters to apply a tenant filter to all database queries.

Implement it once, and you can almost forget about it.

Let's see what are some of the problems we need to solve.

How To Use EF Core Query Filters

If you want an in-depth dive into Query filters , take a look at the newsletter issue where I talked about using query filters with EF Core.

Here's a quick refresher on Query filters :

  • Configure the query filter by calling HasQueryFilter for your entity
  • EF will apply it to all queries for that entity
  • You can turn it off with IgnoreQueryFilters
  • Only one query filter per entity is allowed

And here's a simple example:

modelBuilder
   .Entity<Order>()
   .HasQueryFilter(order => !order.IsDeleted);
Enter fullscreen mode Exit fullscreen mode

All queries to the Order table will include an IsDeleted = FALSE condition.

Single Database Multi-Tenancy With EF Core

You will need two things to implement multi-tenancy on a single database :

  • A way to know who the current tenant is
  • A way to filter the data for that tenant only

The typical approach for multi-tenancy on a single database is having a TenantId column in your tables. And then filtering on that column when querying the database.

You can use the Query filters feature in EF Core to apply a global filter for some entity.

Inside of the OnModelCreating method we configure the query filter on the Order entity:

public class OrdersDbContext : DbContext
{
    private readonly string _tenantId;

    public OrdersDbContext(
        DbContextOptions<OrdersDbContext> options,
        TenantProvider tenantProvider)
        : base(options)
    {
        _tenantId = tenantProvider.TenantId;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<Order>
            .HasQueryFilter(o => o.TenantId == _tenantId);
    }
}
Enter fullscreen mode Exit fullscreen mode

We're using the TenantProvider class to get the current tenant value.

Here's what the TenantProvider implementation looks like:

public sealed class TenantProvider
{
    private const string TenantIdHeaderName = "X-TenantId";

    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantProvider(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string TenantId => _httpContextAccessor
        .HttpContext
        .Request
        .Headers[TenantIdHeaderName];
}
Enter fullscreen mode Exit fullscreen mode

The TenantId is coming from the HTTP request header in this example.

A few other options to get the TenantId are:

  • Query string - api/orders?tenantId=example-tenant-id
  • JWT Claim
  • API Key

If you want a more secure implementation you should go with JWT Claims or API Keys to provide the TenantId value.

Separate Databases Multi-Tenancy With EF Core

What if we want to isolate each tenant to a separate database?

Here are the changes we need to make:

  • Applying different connection string per tenant
  • Resolving the connection string for each tenant somehow

You can't use Query filters here, since we are working with different databases.

So you will need to store the tenant information and connection strings somewhere.

A simple example would be store them in the application settings :

"Tenants": {
    { "Id": "tenant-1", "ConnectionString": "Host=tenant1.db;Database=tenant1" },
    { "Id": "tenant-2", "ConnectionString": "Host=tenant2.db;Database=tenant2" }
}
Enter fullscreen mode Exit fullscreen mode

You can then register an IOptions instance with a list of Tenant objects.

And we need to slightly modify the TenantProvider class to return a connection string for the current tenant:

public sealed class TenantProvider
{
    private const string TenantIdHeaderName = "X-TenantId";

    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly TenantSettings _tenantSettings;

    public TenantProvider(
        IHttpContextAccessor httpContextAccessor,
        IOptions<TenantSettings> tenantsOptions)
    {
        _httpContextAccessor = httpContextAccessor;
        _tenants = tenantsOptions.Value;
    }

    public string TenantId => _httpContextAccessor
        .HttpContext
        .Request
        .Headers[TenantIdHeaderName];

    public string GetConnectionString()
    {
        return _tenantSettings.Tenants.Single(t => t.Id == TenantId);
    }
}
Enter fullscreen mode Exit fullscreen mode

And the last part is registering your DbContext to dynamically resolve the connection string for the current tenant.

builder.Services.AddDbContext<OrdersDbContext>((sp, o) =>
{
    var tenantProvider = sp.GetRequiredService<TenantProvider>();

    var connectionString = tenantProvider.GetConnectionString();

    o.UseSqlServer(connectionString);
});
Enter fullscreen mode Exit fullscreen mode

On every request, we create a new OrdersDbContext and connect to the appropriate database for that tenant.

You should definitely consider storing the tenant connection strings in a secure place like Azure Key Vault.

Closing Thoughts

I hope you now have a better understanding of how to build a multi-tenant system with EF Core.

I showed you the bare bones implementation, which you can improve to make it more robust and secure.

Building multi-tenant systems isn't easy, but when you understand the basic principles it shouldn't be too difficult either.

That's all for this week.

See you next Saturday.


P.S. Whenever you're ready, there are 2 ways I can help you:

  1. Pragmatic Clean Architecture: This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. Join 1,000+ students here.

  2. Patreon Community: Think like a senior software engineer with access to the source code I use in my YouTube videos and exclusive discounts for my courses. Join 850+ engineers here.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .