Multi-Tenant with Code First Ef6

Multi-Tenant With Code First EF6

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

Multi-tenancy in EF6 with multiple schemas having the same tables

  1. Context is connecting to a connection string, if the connection string is resolved at runtime, then you are using One dbcontext class with httprequest specific instance. to distinguish httprequest, host name headers can be used.
  2. There is no easy way to handle migration. This is a complex question, but in short, before the end of release 1, i normally create a initial migration with all scripts to provision the database, this is to help the database created after this change it not effected by any migrations down the track. and then add migration every time i need to change it. let me know if more details needed.

MultiTenancy with DbContext and TenantId - Interceptors, Filters, EF Code-First

I would like to suggest the following approach,
1. Create a column with the name tenant ID for each of the table that contains core business data this is not required for any mapping table.


  1. Use the approach B, by creating an extension method that returns an IQueryable. This method can be an extension of the dbset so that anyone writing a filter clause, can just call this extension method followed by the predicate. This would make the task easier for developers to write code without bothering about tenant ID filter. This particular method will have the code to apply the filter condition for the tenant ID column based on the tenant context in which this query is being executed.

Sample
ctx.TenantFilter().Where(....)


  1. Instead of relying upon the http context you can have tenant ID passed in all of your service methods so that it will be easy for handling the tenant contacts in both the web and the web job applications. This makes a call free from contacts and more easily testable. The multi tenant entity interface approach looks good and we do have a similar limitation in our application which works fine so far.

  2. Regarding adding index you would be required to add an index for tenant ID column in the tables that have tenant ID and that should take care of the DB side query indexing part.

  3. Regarding the authentication part, I would recommend to use asp.net identity 2.0 with the owin pipeline. The system is very extensible customisable and easy to integrate with any external identity providers if need be in future.

  4. Please do take a look at the repository pattern for entity framework which enables you to write lesser code in a generic fashion. This would help us get rid of code duplication and redundancy and very easy to test from unit test cases



Related Topics



Leave a reply



Submit