Efcore: Migration with multi-tenant

Created on 17 Feb 2016  路  10Comments  路  Source: dotnet/efcore

Related to multi tenant implementation ( #4410) i have a couple of problem with migration.
What I have:
A multi tenant configuration where some tables are shared between tenats (ex: Users) other are tenant based (ex: Invoices).
What I need:
Create the right Migrations and have a way to build script at runtime to create per-tenant tables.

I have not yet completed the process it could be interesting to have all this things managed by the PMC have not found a way.

To have at least a part of what i need i have modified my "model building" method adding a parameter to create a model with only shared tables.
From Create method of IDbContextFactory implementation (method used only by Migrations commands) i use two method one to create the full model and one to create only the shared-tables part
but to switch between one to another i have to call "dnx ef migrations add [name]" two times and comment the first call or the otherone. (bad things)

I create all the Tenant-based migration with name that start with "Tenant_" than, at runtime i create a Script only with "Tanant_*" migrations.

Here is the (partial) bad code... obviusly i don't like the "comennt/uncomment" procedure, is there a better way to do the job?

    public class MyContextFactory : IDbContextFactory<ApplicationDbContext>
    {
        public ApplicationDbContext Create()
        {
            string connection = GetConnectionStringHere();

            //return CreateCompleteForTenant("Tenant_Code", connection);   <<< MANUALLY REMOVE COMMENT FOR MIGRATIONS
            //return CreateSharedTblContext( connection);
        }
    }
[...]
public class ApplicationDbContext(): IdentityDbContext<ApplicationUser, IdentityRole, string>

    private static IModel GetCompiled(string tenantSchema, bool SharedOnly)
        {
            //https://github.com/aspnet/EntityFramework/issues/3909
            var serviceCollection = new ServiceCollection();
            serviceCollection.AddEntityFramework().AddSqlServer();
            var serviceProvider = serviceCollection.BuildServiceProvider();
            var coreConventionSetBuilder = new CoreConventionSetBuilder();
            var sqlConventionSetBuilder = new SqlServerConventionSetBuilder(new SqlServerTypeMapper());
            var conventionSet = sqlConventionSetBuilder.AddConventions(coreConventionSetBuilder.CreateConventionSet());

            System.Diagnostics.Debug.WriteLine("TenantTEST: Compile for tenant " + tenantSchema);
            var builder = new ModelBuilder(conventionSet);
        //Shared entities here...
            builder.Entity<UserCompany>();
            (new ApplicationDbContext()).OnModelCreating(builder); //Build Asp.Net Identity model
            if (!SharedOnly)
            {
        //Per-tenant entities here...
                builder.Entity<Invoice>().ToTable("Invoices", tenantSchema);
            }

            return builder.Model;
        }
    public string CreateScriptForNewTenant(string Tenant)
        {
        ...
        }       

Most helpful comment

Hey,

I found an easier way to do this that avoids needing to modify generated migrations...

``` c#
using Microsoft.Data.Entity;
using Microsoft.Data.Entity.Infrastructure;
using Microsoft.Data.Entity.Metadata;
using Microsoft.Data.Entity.Metadata.Conventions.Internal;
using Microsoft.Data.Entity.Migrations;
using Microsoft.Data.Entity.Migrations.Internal;
using Microsoft.Data.Entity.Storage;
using Microsoft.Data.Entity.Storage.Internal;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Repro.RC1
{
public class Program
{
static void Main(string[] args)
{
var factory = new MyContextFactory();

        using (var context = factory.Create("jeff"))
        {
            context.Blogs.Add(new Blog { Url = "sample.com/cats" });
            context.SaveChanges();
        }

        using (var context = factory.Create("jane"))
        {
            context.Blogs.Add(new Blog { Url = "sample.com/dogs" });
            context.Blogs.Add(new Blog { Url = "sample.com/horses" });
            context.SaveChanges();
        }

        using (var context = factory.Create("jane"))
        {
            Console.WriteLine("Jane's Blogs");
            foreach (var blog in context.Blogs)
            {
                Console.WriteLine(" - " + blog.Url);
            }
        }

        using (var context = factory.Create("jeff"))
        {
            Console.WriteLine("Jeff's Blogs");
            foreach (var blog in context.Blogs)
            {
                Console.WriteLine(" - " + blog.Url);
            }
        }
    }
}

public class MyContext : DbContext
{
    public MyContext(string tenant, IServiceProvider provider, DbContextOptions<MyContext> options)
        : base(provider, options)
    {
        Tenant = tenant;
    }

    public string Tenant { get; private set; }

    public DbSet<Blog> Blogs { get; set; }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
    public int Rating { get; set; }
}

public class MyContextFactory : IDbContextFactory<MyContext>
{
    private readonly string _connectionString;
    private readonly IServiceProvider _serviceProvider;
    private readonly ConcurrentDictionary<string, DbContextOptions<MyContext>> _optionsCache;

    public MyContextFactory()
    {
        _connectionString = @"Server=(localdb)\mssqllocaldb;Database=Sample;Trusted_Connection=True;";

        var serviceCollection = new ServiceCollection();
        serviceCollection.AddEntityFramework().AddSqlServer();
        serviceCollection.AddScoped<SqlServerHistoryRepository, TenantHistoryRepository>();
        serviceCollection.AddScoped<SqlServerMigrationsSqlGenerator, TenantMigrationsSqlGenerator>();
        _serviceProvider = serviceCollection.BuildServiceProvider();

        _optionsCache = new ConcurrentDictionary<string, DbContextOptions<MyContext>>();
    }

    // Only used by migrations
    MyContext IDbContextFactory<MyContext>.Create()
    {
        return Create(TenantMigrationsSqlGenerator.TENANT_PLACEHOLDER);
    }

    public MyContext Create(string tenant)
    {
        var cachedOptions = _optionsCache.GetOrAdd(
            tenant,
            t =>
            {
                // Create the options
                var optionsBuilder = new DbContextOptionsBuilder<MyContext>();
                optionsBuilder.UseModel(GetModel(tenant));
                optionsBuilder.UseSqlServer(_connectionString);
                var options = optionsBuilder.Options;

                // Ensure the schema is up to date
                using (var context = new MyContext(tenant, _serviceProvider, options))
                {
                    context.Database.Migrate();
                }

                return options;
            });

        return new MyContext(tenant, _serviceProvider, cachedOptions);
    }

    private static IModel GetModel(string tenant)
    {
        var coreConventionSetBuilder = new CoreConventionSetBuilder();
        var sqlConventionSetBuilder = new SqlServerConventionSetBuilder(new SqlServerTypeMapper());
        var conventionSet = sqlConventionSetBuilder.AddConventions(coreConventionSetBuilder.CreateConventionSet());
        var builder = new ModelBuilder(conventionSet);

        builder.Entity<Blog>()
            .ToTable("blogs", schema: tenant);

        return builder.Model;
    }
}


public class TenantMigrationsSqlGenerator : SqlServerMigrationsSqlGenerator
{
    public static readonly string TENANT_PLACEHOLDER = "tenant_placeholder";

    public TenantMigrationsSqlGenerator(DbContext context, IRelationalCommandBuilderFactory commandBuilderFactory, ISqlGenerator sqlGenerator, IRelationalTypeMapper typeMapper, IRelationalAnnotationProvider annotations)
    : base(commandBuilderFactory, GetSqlGenerator(context, sqlGenerator), typeMapper, annotations)
    { }

    private static ISqlGenerator GetSqlGenerator(DbContext context, ISqlGenerator generator)
    {
        if (context is MyContext)
        {
            var tenant = ((MyContext)context).Tenant;
            return new TenantSqlGenerator(tenant);
        }

        return generator;
    }

    private class TenantSqlGenerator : SqlServerSqlGenerator
    {
        private readonly string _tenant;

        public TenantSqlGenerator(string tenant)
        {
            _tenant = tenant;
        }

        public override string EscapeIdentifier(string identifier)
        {
            if (identifier == TENANT_PLACEHOLDER)
            {
                identifier = _tenant;
            }

            return base.EscapeIdentifier(identifier);
        }
    }
}

public class TenantHistoryRepository : SqlServerHistoryRepository
{
    private readonly string _tenantPrefix;

    public TenantHistoryRepository(DbContext context, IDatabaseCreator databaseCreator, ISqlCommandBuilder sqlCommandBuilder, ISqlServerConnection connection, IDbContextOptions options, IMigrationsModelDiffer modelDiffer, IMigrationsSqlGenerator migrationsSqlGenerator, IRelationalAnnotationProvider annotations, ISqlGenerator sqlGenerator)
        : base(databaseCreator, sqlCommandBuilder, connection, options, modelDiffer, migrationsSqlGenerator, annotations, sqlGenerator)
    {
        if (context is MyContext)
        {
            var tenant = ((MyContext)context).Tenant;
            _tenantPrefix = $"[{tenant}]";
        }
    }

    public override IReadOnlyList<HistoryRow> GetAppliedMigrations()
    {
        // Filter applied migrations to ones with the correct tenant prefix
        if (_tenantPrefix != null)
        {
            return base.GetAppliedMigrations()
                .Where(r => r.MigrationId.StartsWith(_tenantPrefix))
                .Select(r => new HistoryRow(r.MigrationId.Substring(_tenantPrefix.Length), r.ProductVersion))
                .ToList();
        }

        return base.GetAppliedMigrations();
    }

    public override string GetInsertScript(HistoryRow row)
    {
        // Add tenant prefix to any new rows
        if (_tenantPrefix != null)
        {
            row = new HistoryRow(_tenantPrefix + row.MigrationId, row.MigrationId);
        }

        return base.GetInsertScript(row);
    }
}

}
```

All 10 comments

It's not very pretty, but below is some code that shows one way to achieve this in RC1.

Code removed as I found a better way to do this, see two comments below...

Closing as this provides a way to achieve this at the moment. I've opened https://github.com/aspnet/EntityFramework/issues/4625 to provide a way to achieve this more cleanly.

Hey,

I found an easier way to do this that avoids needing to modify generated migrations...

``` c#
using Microsoft.Data.Entity;
using Microsoft.Data.Entity.Infrastructure;
using Microsoft.Data.Entity.Metadata;
using Microsoft.Data.Entity.Metadata.Conventions.Internal;
using Microsoft.Data.Entity.Migrations;
using Microsoft.Data.Entity.Migrations.Internal;
using Microsoft.Data.Entity.Storage;
using Microsoft.Data.Entity.Storage.Internal;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Repro.RC1
{
public class Program
{
static void Main(string[] args)
{
var factory = new MyContextFactory();

        using (var context = factory.Create("jeff"))
        {
            context.Blogs.Add(new Blog { Url = "sample.com/cats" });
            context.SaveChanges();
        }

        using (var context = factory.Create("jane"))
        {
            context.Blogs.Add(new Blog { Url = "sample.com/dogs" });
            context.Blogs.Add(new Blog { Url = "sample.com/horses" });
            context.SaveChanges();
        }

        using (var context = factory.Create("jane"))
        {
            Console.WriteLine("Jane's Blogs");
            foreach (var blog in context.Blogs)
            {
                Console.WriteLine(" - " + blog.Url);
            }
        }

        using (var context = factory.Create("jeff"))
        {
            Console.WriteLine("Jeff's Blogs");
            foreach (var blog in context.Blogs)
            {
                Console.WriteLine(" - " + blog.Url);
            }
        }
    }
}

public class MyContext : DbContext
{
    public MyContext(string tenant, IServiceProvider provider, DbContextOptions<MyContext> options)
        : base(provider, options)
    {
        Tenant = tenant;
    }

    public string Tenant { get; private set; }

    public DbSet<Blog> Blogs { get; set; }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
    public int Rating { get; set; }
}

public class MyContextFactory : IDbContextFactory<MyContext>
{
    private readonly string _connectionString;
    private readonly IServiceProvider _serviceProvider;
    private readonly ConcurrentDictionary<string, DbContextOptions<MyContext>> _optionsCache;

    public MyContextFactory()
    {
        _connectionString = @"Server=(localdb)\mssqllocaldb;Database=Sample;Trusted_Connection=True;";

        var serviceCollection = new ServiceCollection();
        serviceCollection.AddEntityFramework().AddSqlServer();
        serviceCollection.AddScoped<SqlServerHistoryRepository, TenantHistoryRepository>();
        serviceCollection.AddScoped<SqlServerMigrationsSqlGenerator, TenantMigrationsSqlGenerator>();
        _serviceProvider = serviceCollection.BuildServiceProvider();

        _optionsCache = new ConcurrentDictionary<string, DbContextOptions<MyContext>>();
    }

    // Only used by migrations
    MyContext IDbContextFactory<MyContext>.Create()
    {
        return Create(TenantMigrationsSqlGenerator.TENANT_PLACEHOLDER);
    }

    public MyContext Create(string tenant)
    {
        var cachedOptions = _optionsCache.GetOrAdd(
            tenant,
            t =>
            {
                // Create the options
                var optionsBuilder = new DbContextOptionsBuilder<MyContext>();
                optionsBuilder.UseModel(GetModel(tenant));
                optionsBuilder.UseSqlServer(_connectionString);
                var options = optionsBuilder.Options;

                // Ensure the schema is up to date
                using (var context = new MyContext(tenant, _serviceProvider, options))
                {
                    context.Database.Migrate();
                }

                return options;
            });

        return new MyContext(tenant, _serviceProvider, cachedOptions);
    }

    private static IModel GetModel(string tenant)
    {
        var coreConventionSetBuilder = new CoreConventionSetBuilder();
        var sqlConventionSetBuilder = new SqlServerConventionSetBuilder(new SqlServerTypeMapper());
        var conventionSet = sqlConventionSetBuilder.AddConventions(coreConventionSetBuilder.CreateConventionSet());
        var builder = new ModelBuilder(conventionSet);

        builder.Entity<Blog>()
            .ToTable("blogs", schema: tenant);

        return builder.Model;
    }
}


public class TenantMigrationsSqlGenerator : SqlServerMigrationsSqlGenerator
{
    public static readonly string TENANT_PLACEHOLDER = "tenant_placeholder";

    public TenantMigrationsSqlGenerator(DbContext context, IRelationalCommandBuilderFactory commandBuilderFactory, ISqlGenerator sqlGenerator, IRelationalTypeMapper typeMapper, IRelationalAnnotationProvider annotations)
    : base(commandBuilderFactory, GetSqlGenerator(context, sqlGenerator), typeMapper, annotations)
    { }

    private static ISqlGenerator GetSqlGenerator(DbContext context, ISqlGenerator generator)
    {
        if (context is MyContext)
        {
            var tenant = ((MyContext)context).Tenant;
            return new TenantSqlGenerator(tenant);
        }

        return generator;
    }

    private class TenantSqlGenerator : SqlServerSqlGenerator
    {
        private readonly string _tenant;

        public TenantSqlGenerator(string tenant)
        {
            _tenant = tenant;
        }

        public override string EscapeIdentifier(string identifier)
        {
            if (identifier == TENANT_PLACEHOLDER)
            {
                identifier = _tenant;
            }

            return base.EscapeIdentifier(identifier);
        }
    }
}

public class TenantHistoryRepository : SqlServerHistoryRepository
{
    private readonly string _tenantPrefix;

    public TenantHistoryRepository(DbContext context, IDatabaseCreator databaseCreator, ISqlCommandBuilder sqlCommandBuilder, ISqlServerConnection connection, IDbContextOptions options, IMigrationsModelDiffer modelDiffer, IMigrationsSqlGenerator migrationsSqlGenerator, IRelationalAnnotationProvider annotations, ISqlGenerator sqlGenerator)
        : base(databaseCreator, sqlCommandBuilder, connection, options, modelDiffer, migrationsSqlGenerator, annotations, sqlGenerator)
    {
        if (context is MyContext)
        {
            var tenant = ((MyContext)context).Tenant;
            _tenantPrefix = $"[{tenant}]";
        }
    }

    public override IReadOnlyList<HistoryRow> GetAppliedMigrations()
    {
        // Filter applied migrations to ones with the correct tenant prefix
        if (_tenantPrefix != null)
        {
            return base.GetAppliedMigrations()
                .Where(r => r.MigrationId.StartsWith(_tenantPrefix))
                .Select(r => new HistoryRow(r.MigrationId.Substring(_tenantPrefix.Length), r.ProductVersion))
                .ToList();
        }

        return base.GetAppliedMigrations();
    }

    public override string GetInsertScript(HistoryRow row)
    {
        // Add tenant prefix to any new rows
        if (_tenantPrefix != null)
        {
            row = new HistoryRow(_tenantPrefix + row.MigrationId, row.MigrationId);
        }

        return base.GetInsertScript(row);
    }
}

}
```

Thank you very much!
It seems to be perfect! Now I have to try this code.
thank you.

Here are some minor bugs in the sample code @rowanmiller provided above.

1) It will generate the following SQL when ensuring that the tenant schema exists:

IF SCHEMA_ID(N'tenant_placeholder') IS NULL EXEC(N'CREATE SCHEMA [jeff]');
go

To generate the correct SQL, I also had to override GenerateLiteralValue in TenantMigrationsSqlGenerator as follows:

protected override string GenerateLiteralValue(string value, bool unicode = true)
            {
                if (value == TENANT_PLACEHOLDER)
                {
                    value = _tenant;
                }

                return base.GenerateLiteralValue(value, unicode);
            }

2) There is a small bug in the sample code row = new HistoryRow(_tenantPrefix + row.MigrationId, row.MigrationId);should be row = new HistoryRow(_tenantPrefix + row.MigrationId, row.ProductVersion);

ISqlGenerator is ISqlGenerationHelper (in RC2)
and
SqlServerSqlGenerator is SqlServerSqlGenerationHelper (in RC2).
But how to use this code with RC2?

I am getting a System.InvalidOperationException: Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContext' while attempting to activate 'MultiTenant.Demo.TenantHistoryRepository'.

`namespace MultiTenant.Demo
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

using JetBrains.Annotations;

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Microsoft.Extensions.DependencyInjection;

[UsedImplicitly]
internal class Program
{
    [UsedImplicitly]
    private static void Main()
    {
        var factory = new MyContextFactory();

        using (var context = factory.Create("jeff"))
        {
            context.Blogs.Add(new Blog { Url = "sample.com/cats" });
            context.SaveChanges();
        }

        using (var context = factory.Create("jane"))
        {
            context.Blogs.Add(new Blog { Url = "sample.com/dogs" });
            context.Blogs.Add(new Blog { Url = "sample.com/horses" });
            context.SaveChanges();
        }

        using (var context = factory.Create("jane"))
        {
            Console.WriteLine("Jane's Blogs");
            foreach (var blog in context.Blogs)
            {
                Console.WriteLine(" - " + blog.Url);
            }
        }

        using (var context = factory.Create("jeff"))
        {
            Console.WriteLine("Jeff's Blogs");
            foreach (var blog in context.Blogs)
            {
                Console.WriteLine(" - " + blog.Url);
            }
        }
    }
}

public class MyContext : DbContext
{
    public MyContext()
    {            
    }

    public MyContext(string tenant, DbContextOptions<MyContext> options)
        : base(options)
    {
        this.Tenant = tenant;
    }

    public string Tenant { get; }

    public DbSet<Blog> Blogs { get; [UsedImplicitly] set; }
}

public class Blog
{
    [UsedImplicitly]
    public int BlogId { get; set; }
    public string Url { get; set; }
    [UsedImplicitly]
    public int Rating { get; set; }
}

public class MyContextFactory : IDbContextFactory<MyContext>
{
    private readonly string connectionString;
    private readonly IServiceProvider serviceProvider;
    private readonly ConcurrentDictionary<string, DbContextOptions<MyContext>> optionsCache;

    public MyContextFactory()
    {
        this.connectionString = @"Server=(localdb)\mssqllocaldb;Database=Sample;Trusted_Connection=True;";

        var serviceCollection = new ServiceCollection();
        serviceCollection.AddEntityFrameworkSqlServer();
        serviceCollection.AddScoped<SqlServerHistoryRepository, TenantHistoryRepository>();
        serviceCollection.AddScoped<SqlServerMigrationsSqlGenerator, TenantMigrationsSqlGenerator>();
        this.serviceProvider = serviceCollection.BuildServiceProvider();

        this.optionsCache = new ConcurrentDictionary<string, DbContextOptions<MyContext>>();
    }


    public MyContext Create(DbContextFactoryOptions options)
    {
        return this.Create(TenantMigrationsSqlGenerator.TenantPlaceholder);
    }

    public MyContext Create(string tenant)
    {
        var cachedOptions = this.optionsCache.GetOrAdd(
            tenant,
            t =>
            {
                // Create the options
                var optionsBuilder = new DbContextOptionsBuilder<MyContext>();
                optionsBuilder.UseModel(GetModel(tenant));
                optionsBuilder.UseSqlServer(this.connectionString);
                optionsBuilder.UseInternalServiceProvider(this.serviceProvider);
                var options = optionsBuilder.Options;

                // Ensure the schema is up to date
                using (var context = new MyContext(tenant, options))
                {
                    context.Database.Migrate();
                }

                return options;
            });

        return new MyContext(tenant, cachedOptions);
    }

    private static IModel GetModel(string tenant)
    {
        var coreConventionSetBuilder = new CoreConventionSetBuilder();
        var sqlConventionSetBuilder = new SqlServerConventionSetBuilder(new SqlServerTypeMapper(), null, null);
        var conventionSet = sqlConventionSetBuilder.AddConventions(coreConventionSetBuilder.CreateConventionSet());
        var builder = new ModelBuilder(conventionSet);

        builder.Entity<Blog>()
            .ToTable("blogs", schema: tenant);

        return builder.Model;
    }
}


[UsedImplicitly]
public class TenantMigrationsSqlGenerator : SqlServerMigrationsSqlGenerator
{
    public static readonly string TenantPlaceholder = "tenant_placeholder";

    public TenantMigrationsSqlGenerator(DbContext context, [NotNull] IRelationalCommandBuilderFactory commandBuilderFactory, [NotNull] ISqlGenerationHelper sqlGenerationHelper, [NotNull] IRelationalTypeMapper typeMapper, [NotNull] IRelationalAnnotationProvider annotations) 
        : base(commandBuilderFactory, GetSqlGenerator(context, sqlGenerationHelper), typeMapper, annotations)
    {
    }

    private static ISqlGenerationHelper GetSqlGenerator(DbContext context, ISqlGenerationHelper generator)
    {
        var myContext = context as MyContext;
        if (myContext == null) return generator;
        var tenant = myContext.Tenant;
        return new TenantSqlGenerator(tenant);
    }

    private class TenantSqlGenerator : SqlServerSqlGenerationHelper
    {
        private readonly string tenant;

        public TenantSqlGenerator(string tenant)
        {
            this.tenant = tenant;
        }

        public override string EscapeIdentifier(string identifier)
        {
            if (identifier == TenantPlaceholder)
            {
                identifier = this.tenant;
            }

            return base.EscapeIdentifier(identifier);
        }
    }
}

[UsedImplicitly]
public class TenantHistoryRepository : SqlServerHistoryRepository
{
    private readonly string tenantPrefix;

    public TenantHistoryRepository(DbContext context, [NotNull] IDatabaseCreator databaseCreator, [NotNull] IRawSqlCommandBuilder rawSqlCommandBuilder, [NotNull] ISqlServerConnection connection, [NotNull] IDbContextOptions options, [NotNull] IMigrationsModelDiffer modelDiffer, [NotNull] IMigrationsSqlGenerator migrationsSqlGenerator, [NotNull] IRelationalAnnotationProvider annotations, [NotNull] ISqlGenerationHelper sqlGenerationHelper) : base(databaseCreator, rawSqlCommandBuilder, connection, options, modelDiffer, migrationsSqlGenerator, annotations, sqlGenerationHelper)
    {
        var myContext = context as MyContext;
        if (myContext == null) return;
        var tenant = myContext.Tenant;
        this.tenantPrefix = $"[{tenant}]";
    }


    public override IReadOnlyList<HistoryRow> GetAppliedMigrations()
    {
        // Filter applied migrations to ones with the correct tenant prefix
        if (this.tenantPrefix != null)
        {
            return base.GetAppliedMigrations()
                .Where(r => r.MigrationId.StartsWith(this.tenantPrefix))
                .Select(r => new HistoryRow(r.MigrationId.Substring(this.tenantPrefix.Length), r.ProductVersion))
                .ToList();
        }

        return base.GetAppliedMigrations();
    }

    public override string GetInsertScript(HistoryRow row)
    {
        // Add tenant prefix to any new rows
        if (this.tenantPrefix != null)
        {
            row = new HistoryRow(this.tenantPrefix + row.MigrationId, row.ProductVersion);
        }

        return base.GetInsertScript(row);
    }
}

}`

@boutquin You can let your TenantHistoryRepository depend on ICurrentDbContext instead of DbContext and then access your context from CurrentDbContext.Context.

Thanks for this great stuff @rowanmiller & @rmja!

IDbContextFactory appears to be obsolete.

Is there any documentation on how to have the databases dynamically generated per tenant (in my scenario each tenant has a separate DB).

Related https://github.com/aspnet/EntityFramework.Docs/issues/777.

Thank you!

Hi @rowanmiller and @rmja !

Thanks for the great examples provided in this topic.

I've got a question: given all the changes between the EF Core version used here and current 3.1 version, how should be this code adapted (assuming it's possible)?

For example:

  • IDbContextFactory is marked as obsolete
  • SqlServerSqlGenerationHelper requires an instance of RelationalSqlGenerationHelperDependencies object
  • SqlServerMigrationsSqlGenerator ctor's parameters changed

Thank in advance for any help.

Was this page helpful?
0 / 5 - 0 ratings