Multi-tenant ASP.NET Core 5 - Implementing database per tenant strategy
Open Source Your Knowledge, Become a Contributor
Technology knowledge has to be shared and made accessible for free. Join the movement.
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.
References
- Implementing database per tenant strategy on ASP.NET Core (Gunnar Peipman)
- Global query filters in Entity Framework Core 2.0 (Gunnar Peipman)
- Implementing tenant providers on ASP.NET Core (Gunnar Peipman)