I'm working with .NET Core 3.1 and EntityFramework Core 3.1.3. I'm trying to implement tenant data separation using DB schemas. I've read this. I'm aware it's a little bit outdated, so I've adjusted.
I've created an implementation of DbContext:
public class AppDataContext : DbContext
{
private readonly ITenantProvider _tenantProvider;
public AppDataContext(DbContextOptions<AppDataContext> options, ITenantProvider tenantProvider) : base(options)
{
_tenantProvider = tenantProvider;
}
public DbSet<Book> Books { get; set; }
public DbSet<Comics> Comics { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Tenant schema mapping
var tenant = _tenantProvider.GetTenantString();
modelBuilder.HasDefaultSchema(tenant);
}
public static void ApplyMigrations(string connectionString, string tenant)
{
var optionsBuilder = new DbContextOptionsBuilder<AppDataContext>();
optionsBuilder.UseSqlServer(connectionString, x => x.MigrationsHistoryTable("__EFMigrationsHistory", tenant));
var ctx = new AppDataContext(optionsBuilder.Options, StaticTenantProvider.WithTenant(tenant));
ctx.Database.Migrate();
}
}
ITenantProvider is a registered scoped service in .NET Core DI. This AppDataContext is registered like so:
services.AddDbContext<AppDataContext>((ctx, opt) =>
{
opt.UseSqlServer(Configuration["SqlConnectionString"]);
});
Now, my idea would be, that anytime I want to provision another tenant, I would make call like this (database instance already exists and may have some tables/schemas already):
var connString = GetConnString(); // Where does connection string and tenant name come from is not important
var tenantName = GetTenantName();
AppDataContext.ApplyMigrations(connString, tenantName);
And after this I would have a new unique DB schema with all tables all set up for the Tenant.
Unfortunately, it's not working. Tables are still being created for default "dbo" schema (I'm using SqlServer).
I've started looking around the internet, first I've found this. It looked a bit odd for me, but tried anyway:
public static void ApplyMigrations(string connectionString, string tenant)
{
var optionsBuilder = new DbContextOptionsBuilder<AppDataContext>();
optionsBuilder.UseSqlServer(connectionString);
var ctx = new AppDataContext(optionsBuilder.Options, StaticTenantProvider.WithTenant(tenant));
var command = $"IF (NOT EXISTS (SELECT * FROM sys.schemas WHERE name = N'{tenant}')) " +
"BEGIN" +
$" EXEC ('CREATE SCHEMA {tenant}');" +
"END";
ctx.Database.ExecuteSqlRaw(command);
ctx.Database.Migrate();
}
Didn't work. I could see that the schema got created (using SSMS), but the tables still landed in "dbo".
Then I thought, that maybe it has problems with locating the Migration History table, so I tried MigrationsHistoryTable like this:
optionsBuilder.UseSqlServer(connectionString, x => x.MigrationsHistoryTable("__EFMigrationsHistory", tenant));
But no, Migration History table did get created in new schema, but all other tables still were in "dbo".
What am I missing? Or maybe it's not possible to use EF Migrations and Schema separation at the same time?
Thanks in advance.
EDIT: To make things clear: I'm using Code First approach. There are already generated migrations for AppDataContext.