Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multi-Tenant With Code First EF6

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

There is a great article here on getting started with this kind of thing here: http://romiller.com/2011/05/23/ef-4-1-multi-tenant-with-code-first/

In the middle of the article, this is written:

You’ll notice (probably with some dismay) we need to write code to configure the table schema for each entity. Admittedly there aren’t many magic unicorns grazing around this code… in future versions of EF we will be able to replace this with a much cleaner custom convention.

Our goal is to have the cleanest way possible of having a single context class that we can use to connect to multiple schemas that have the same model.
(Note that modelBuilder.HasDefaultSchema doesn't seem sufficient, because it only applies that the first time EF initializes the context and runs OnModelCreating)

Does the aforementioned cleaner custom convention exist in EF5 or EF6?
Or is there a cleaner way to handle this somehow?

Note: I also asked this question on the development forum, as it seems to relate more to the direction of EF, but wanted to see if anybody here had alternatives.

Note2: I'm not worried about migrations, we'll handle that seperately.

like image 499
Kevin Radcliffe Avatar asked Oct 18 '13 20:10

Kevin Radcliffe


People also ask

What is multi-tenant code?

Multi-Tenant – Multi-tenancy means that a single instance of the software and its supporting infrastructure serves multiple customers. Each customer shares the software application and also shares a single database. Each tenant's data is isolated and remains invisible to other tenants.

How do you handle multi-tenancy in spring boot?

There are three approaches on how to build a multi-tenant application: Database per tenant — each tenant has its database. Shared database, separate schema — all tenants are using the same database, but each tenant has his schema. Shared database, shared schema — all tenants are using the same schema.

How multi-tenancy is implemented?

We can implement multi-tenancy using any of the following approaches: Database per Tenant: Each Tenant has its own database and is isolated from other tenants. Shared Database, Shared Schema: All Tenants share a database and tables. Every table has a Column with the Tenant Identifier, that shows the owner of the row.

What is multi-tenant authentication?

Multi-tenant authentication helps simplify the whole process by enabling a user to easily authenticate to a database by using a valid user ID despite any tenancy.


1 Answers

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<TenantDataCtx>(null);         }          internal TenantDataCtx(string connectionString, Guid tenantId)             : base(connectionString)         {             Database.SetInitializer<TenantDataCtx>(null);             this.SchemaName = tenantId.ToString("D");         }          public string SchemaName { get; private set; }          #endregion          #region DataSet Properties          public DbSet<TestEntity> TestEntities { get { return this.Set<TestEntity>(); } }          #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<TenantDataContext.TenantDataCtx>(); 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

like image 86
Tobias J. Avatar answered Sep 19 '22 10:09

Tobias J.