Multi-Tenant With Code First EF6

后端 未结 4 1891
小鲜肉
小鲜肉 2020-11-27 13:55

Our organization has need to have a single database, multi-tenant
(by table schema, not by tenant id) architecture.

There is a great article h

4条回答
  •  谎友^
    谎友^ (楼主)
    2020-11-27 14:43

    The property modelBuilder.HasDefaultSchema in OnModelCreating is sufficient if you implement IDbModelCacheKeyProvider on your DbContext. A model is created once and than cached internally by EntityFramwork and you can define your own key for the cache. Take the schema name as model cache key and EF will create a model by every different cache key (schema in our case). Here is my proof of concept code:

    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Infrastructure;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    using TenantDataModel;
    
    namespace TenantDataContext
    {
        public class TenantDataCtx : DbContext, IDbModelCacheKeyProvider
        {
            #region Construction
    
            public static TenantDataCtx Create(string databaseServer, string databaseName, string databaseUserName, string databasePassword, Guid tenantId)
            {
                var connectionStringBuilder = new System.Data.SqlClient.SqlConnectionStringBuilder();
                connectionStringBuilder.DataSource = databaseServer;
                connectionStringBuilder.InitialCatalog = databaseName;
                connectionStringBuilder.UserID = databaseUserName;
                connectionStringBuilder.Password = databasePassword;
    
                string connectionString = connectionStringBuilder.ToString();
                return new TenantDataCtx(connectionString, tenantId);
            }
    
            // Used by EF migrations
            public TenantDataCtx()
            {
                Database.SetInitializer(null);
            }
    
            internal TenantDataCtx(string connectionString, Guid tenantId)
                : base(connectionString)
            {
                Database.SetInitializer(null);
                this.SchemaName = tenantId.ToString("D");
            }
    
            public string SchemaName { get; private set; }
    
            #endregion
    
            #region DataSet Properties
    
            public DbSet TestEntities { get { return this.Set(); } }
    
            #endregion
    
            #region Overrides
    
            protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                if (this.SchemaName != null)
                {
                    modelBuilder.HasDefaultSchema(this.SchemaName);
                }
    
                base.OnModelCreating(modelBuilder);
            }
    
            #endregion
    
            #region IDbModelCacheKeyProvider Members
    
            public string CacheKey
            {
                get { return this.SchemaName; }
            }
    
            #endregion
        }
    }
    

    Furthermore I have found a way to use EF migrations. I am not really happy with my solution but it seems that there are no other solutions available right now.

    using System;
    using System.Collections.Generic;
    using System.Data.Entity.SqlServer;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace TenantDatabaseManager
    {
        public class SqlServerSchemaAwareMigrationSqlGenerator : SqlServerMigrationSqlGenerator
        {
            private string _schema;
    
            public SqlServerSchemaAwareMigrationSqlGenerator(string schema)
            {
                _schema = schema;
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.AddColumnOperation addColumnOperation)
            {
                string newTableName = _GetNameWithReplacedSchema(addColumnOperation.Table);
                var newAddColumnOperation = new System.Data.Entity.Migrations.Model.AddColumnOperation(newTableName, addColumnOperation.Column, addColumnOperation.AnonymousArguments);
                base.Generate(newAddColumnOperation);
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.AddPrimaryKeyOperation addPrimaryKeyOperation)
            {
                addPrimaryKeyOperation.Table = _GetNameWithReplacedSchema(addPrimaryKeyOperation.Table);
                base.Generate(addPrimaryKeyOperation);
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.AlterColumnOperation alterColumnOperation)
            {
                string tableName = _GetNameWithReplacedSchema(alterColumnOperation.Table);
                var newAlterColumnOperation = new System.Data.Entity.Migrations.Model.AlterColumnOperation(tableName, alterColumnOperation.Column, alterColumnOperation.IsDestructiveChange);
                base.Generate(newAlterColumnOperation);
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.DropPrimaryKeyOperation dropPrimaryKeyOperation)
            {
                dropPrimaryKeyOperation.Table = _GetNameWithReplacedSchema(dropPrimaryKeyOperation.Table);
                base.Generate(dropPrimaryKeyOperation);
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.CreateIndexOperation createIndexOperation)
            {
                string name = _GetNameWithReplacedSchema(createIndexOperation.Table);
                createIndexOperation.Table = name;
                base.Generate(createIndexOperation);
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.CreateTableOperation createTableOperation)
            {
                string newTableName = _GetNameWithReplacedSchema(createTableOperation.Name);
                var newCreateTableOperation = new System.Data.Entity.Migrations.Model.CreateTableOperation(newTableName, createTableOperation.AnonymousArguments);
                newCreateTableOperation.PrimaryKey = createTableOperation.PrimaryKey;
                foreach (var column in createTableOperation.Columns)
                {
                    newCreateTableOperation.Columns.Add(column);
                }
    
                base.Generate(newCreateTableOperation);
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.RenameTableOperation renameTableOperation)
            {
                string oldName = _GetNameWithReplacedSchema(renameTableOperation.Name);
                string newName = renameTableOperation.NewName.Split(new char[] { '.' }).Last();
                var newRenameTableOperation = new System.Data.Entity.Migrations.Model.RenameTableOperation(oldName, newName, renameTableOperation.AnonymousArguments);
                base.Generate(newRenameTableOperation);
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.RenameIndexOperation renameIndexOperation)
            {
                string tableName = _GetNameWithReplacedSchema(renameIndexOperation.Table);
                var newRenameIndexOperation = new System.Data.Entity.Migrations.Model.RenameIndexOperation(tableName, renameIndexOperation.Name, renameIndexOperation.NewName);
                base.Generate(newRenameIndexOperation);
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.AddForeignKeyOperation addForeignKeyOperation)
            {
                addForeignKeyOperation.DependentTable = _GetNameWithReplacedSchema(addForeignKeyOperation.DependentTable);
                addForeignKeyOperation.PrincipalTable = _GetNameWithReplacedSchema(addForeignKeyOperation.PrincipalTable);
                base.Generate(addForeignKeyOperation);
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.DropColumnOperation dropColumnOperation)
            {
                string newTableName = _GetNameWithReplacedSchema(dropColumnOperation.Table);
                var newDropColumnOperation = new System.Data.Entity.Migrations.Model.DropColumnOperation(newTableName, dropColumnOperation.Name, dropColumnOperation.AnonymousArguments);
                base.Generate(newDropColumnOperation);
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.RenameColumnOperation renameColumnOperation)
            {
                string newTableName = _GetNameWithReplacedSchema(renameColumnOperation.Table);
                var newRenameColumnOperation = new System.Data.Entity.Migrations.Model.RenameColumnOperation(newTableName, renameColumnOperation.Name, renameColumnOperation.NewName);
                base.Generate(newRenameColumnOperation);
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.DropTableOperation dropTableOperation)
            {
                string newTableName = _GetNameWithReplacedSchema(dropTableOperation.Name);
                var newDropTableOperation = new System.Data.Entity.Migrations.Model.DropTableOperation(newTableName, dropTableOperation.AnonymousArguments);
                base.Generate(newDropTableOperation);
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.DropForeignKeyOperation dropForeignKeyOperation)
            {
                dropForeignKeyOperation.PrincipalTable = _GetNameWithReplacedSchema(dropForeignKeyOperation.PrincipalTable);
                dropForeignKeyOperation.DependentTable = _GetNameWithReplacedSchema(dropForeignKeyOperation.DependentTable);
                base.Generate(dropForeignKeyOperation);
            }
    
            protected override void Generate(System.Data.Entity.Migrations.Model.DropIndexOperation dropIndexOperation)
            {
                dropIndexOperation.Table = _GetNameWithReplacedSchema(dropIndexOperation.Table);
                base.Generate(dropIndexOperation);
            }
    
            private string _GetNameWithReplacedSchema(string name)
            {
                string[] nameParts = name.Split('.');
                string newName;
    
                switch (nameParts.Length)
                {
                    case 1:
                        newName = string.Format("{0}.{1}", _schema, nameParts[0]);
                        break;
    
                    case 2:
                        newName = string.Format("{0}.{1}", _schema, nameParts[1]);
                        break;
    
                    case 3:
                        newName = string.Format("{0}.{1}.{2}", _schema, nameParts[1], nameParts[2]);
                        break;
    
                    default:
                        throw new NotSupportedException();
                }
    
                return newName;
            }
        }
    }
    

    And this is how I use the SqlServerSchemaAwareMigrationSqlGenerator:

    // Update TenantDataCtx
    var tenantDataMigrationsConfiguration = new DbMigrationsConfiguration();
    tenantDataMigrationsConfiguration.AutomaticMigrationsEnabled = false;
    tenantDataMigrationsConfiguration.SetSqlGenerator("System.Data.SqlClient", new SqlServerSchemaAwareMigrationSqlGenerator(schemaName));
    tenantDataMigrationsConfiguration.SetHistoryContextFactory("System.Data.SqlClient", (existingConnection, defaultSchema) => new HistoryContext(existingConnection, schemaName));
    tenantDataMigrationsConfiguration.TargetDatabase = new System.Data.Entity.Infrastructure.DbConnectionInfo(connectionString, "System.Data.SqlClient");
    tenantDataMigrationsConfiguration.MigrationsAssembly = typeof(TenantDataContext.TenantDataCtx).Assembly;
    tenantDataMigrationsConfiguration.MigrationsNamespace = "TenantDataContext.Migrations.TenantData";
    
    DbMigrator tenantDataCtxMigrator = new DbMigrator(tenantDataMigrationsConfiguration);
    tenantDataCtxMigrator.Update();
    

    Regards from Germany,

    Tobias

提交回复
热议问题