Multi-tenant ASP.NET Core 5 - Implementing database per tenant strategy

gpeipman
1,055 views

Open Source Your Knowledge, Become a Contributor

Technology knowledge has to be shared and made accessible for free. Join the movement.

Create Content

Implementing database per tenant strategy

It's possible that in multi-tenant web application each tenant has its own database. This example shows how to do it.

We start with some code artifacts needed to build such solution:

  • Tenant class - holds current tenant information like metadata and settings
  • Tenant provider interface - interface that tenant providers implement
  • Tenant provider - dummy tenant provider to return demo tenant
  • Person class - simple entity for demo purposes
public class Tenant
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Host { get; set; }
    public string DatabaseConnectionString { get; set; }
}

public interface ITenantProvider
{
    Tenant GetTenant();
}

public class DummyTenantProvider : ITenantProvider
{
    private static IList<Tenant> _tenants = new List<Tenant>
    {
        new Tenant { Id = MultitenantDbContext.Tenant1Id, Name = "Imaginary corp", DatabaseConnectionString = "ConnStr1" },
        new Tenant { Id = MultitenantDbContext.Tenant2Id, Name = "The Very Big corp", DatabaseConnectionString = "ConnStr2" },
    };

    public Tenant GetTenant()
    {
        return _tenants.First();
    }
}

public class Person
{
    public Guid Id { get; set; }
    public Guid TenantId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Registering services

Let's register services when web application starts. It's done in ConfigureServices() method of Startup class.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<MultitenantDbContext>(o => { });

    services.AddMvc();

    services.AddTransient<ITenantProvider, DummyTenantProvider>();
}

We don't set database connection string here as we want to do it in database context when current tenant is detected.

Building multi-tenant database context

Now it's time to build database context.

public class MultitenantDbContext : DbContext
{
    public static Guid Tenant1Id = Guid.Parse("51aab199-1482-4f0d-8ff1-5ca0e7bc525a");
    public static Guid Tenant2Id = Guid.Parse("ae4e21fa-57cb-4733-b971-fdd14c4c667e");

    public DbSet<Person> People { get; set; }

    private Tenant _tenant;
    private ILogger<MultitenantDbContext> _logger;

    public MultitenantDbContext(ITenantProvider tenantProvider, ILogger<MultitenantDbContext> logger)
    {
        _tenant = tenantProvider.GetTenant();
        _logger = logger;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // Comment out for real application
        //optionsBuilder.UseSqlServer(_tenant.DatabaseConnectionString);

        // Comment in for real applications
        optionsBuilder.UseInMemoryDatabase(Guid.NewGuid().ToString());

        base.OnConfiguring(optionsBuilder);

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

        modelBuilder.Entity<Person>().HasQueryFilter(p => p.TenantId == _tenant.Id);
    }

    public void AddSampleData()
    {
        People.Add(new Person
        {
            Id = Guid.Parse("79865406-e01b-422f-bd09-92e116a0664a"),
            TenantId = Tenant1Id,
            FirstName = "Gunnar",
            LastName = "Peipman"
        });

        People.Add(new Person
        {
            Id = Guid.Parse("d5674750-7f6b-43b9-b91b-d27b7ac13572"),
            TenantId = Tenant2Id,
            FirstName = "John",
            LastName = "Doe"
        });

        People.Add(new Person
        {
            Id = Guid.Parse("e41446f9-c779-4ff6-b3e5-752a3dad97bb"),
            TenantId = Tenant1Id,
            FirstName = "Mary",
            LastName = "Jones"
        });

        SaveChanges();
    }
}

Check OnConfiguring() method. This is where connection string is set for database provider. First tenant is detected by tenant provider and then tenant specific connection string is used by database context.

Demo

Let's run demo that writes out current tenant people and connection string.

Click Run to run the demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using System.Linq;
using DBPerTenantExample.Models;
using Microsoft.AspNetCore.Mvc;
namespace DBPerTenantExample.Controllers
{
public class HomeController : Controller
{
private readonly MultitenantDbContext _context;
private readonly ITenantProvider _provider;
public HomeController(MultitenantDbContext context, ITenantProvider provider)
{
_context = context;
_context.AddSampleData();
_provider = provider;
}
public IActionResult Index()
{
var model = new IndexViewModel();
model.People = _context.People.ToList();
model.Tenant = _provider.GetTenant();
return View(model);
}
public IActionResult Error()
{
return View();
}
}
}
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

References

Open Source Your Knowledge: become a Contributor and help others learn. Create New Content