Entity Framework Multitenancy Part 2 – Conventions
In my last post, I talked about different scenarios for achieving multitenancy with Entity Framework contexts. This time, I am going to show how to use conventions, wrapping the code I provided then.
First, a base convention class to serve as our root hierarchy of multitenant conventions:
public abstract class MultitenantConvention : IConvention
{}
Only worthy of mention is the implementation of IConvention. This is a marker interface for letting Entity Framework know that this is a convention.
Next, a convention for the separate databases approach:
public class SeparateDatabasesConvention : MultitenantConvention
{ public SeparateDatabasesConvention(DbContext ctx) {var currentTenantId = TenantConfiguration.GetCurrentTenant();
ctx.Database.Connection.ConnectionString = ConfigurationManager.ConnectionStrings[currentTenantId].ConnectionString;
}
}
This convention needs a reference to the DbContext, because it needs to change the connection string dynamically, something that you can’t do through the basic convention interfaces and classes – there’s no way to get to the context.
Now, shared database, different schemas. This time, we need to implement IStoreModelConvention<T>, using EntitySet as the generic parameter, so as to gain access to the Schema property:
public class SharedDatabaseSeparateSchemaConvention : MultitenantConvention, IStoreModelConvention<EntitySet>
{public void Apply(EntitySet item, DbModel model)
{var currentTenantId = TenantConfiguration.GetCurrentTenant();
item.Schema = currentTenantId;
}
}
Finally, shared database, shared schema:
public class SharedDatabaseSharedSchemaConvention : MultitenantConvention
{public String DiscriminatorColumnName { get; private set; }
private void Map<T>(EntityMappingConfiguration<T> cfg) where T : class
{var currentTenantId = TenantConfiguration.GetCurrentTenant();
cfg.Requires(this.DiscriminatorColumnName).HasValue(currentTenantId);}
public SharedDatabaseSharedSchemaConvention(DbModelBuilder modelBuilder, String discriminatorColumnName = "Tenant")
{ this.DiscriminatorColumnName = discriminatorColumnName;var modelConfiguration = modelBuilder.GetType().GetProperty("ModelConfiguration", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(modelBuilder, null);
var entities = modelConfiguration.GetType().GetProperty("Entities", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(modelConfiguration, null) as IEnumerable<Type>;
foreach (var entity in entities)
{var entityTypeConfiguration = modelBuilder.GetType().GetMethod("Entity").MakeGenericMethod(entity).Invoke(modelBuilder, null);
var mapMethod = entityTypeConfiguration.GetType().GetMethods().First(m => m.Name == "Map");var localMethod = this.GetType().GetMethod("Map", BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(entity);
var delegateType = typeof(Action<>).MakeGenericType(localMethod.GetParameters().First().ParameterType); var del = Delegate.CreateDelegate(delegateType, this, localMethod); mapMethod.Invoke(entityTypeConfiguration, new Object[] { del });}
}
}
And a nice way to wrap all this using extension methods:
public static class DbModelBuilderExtensions
{public static DbModelBuilder UseSeparateDatabases(this DbModelBuilder modelBuilder, DbContext ctx)
{modelBuilder.Conventions.Remove<MultitenantConvention>();
modelBuilder.Conventions.Add(new SeparateDatabasesConvention(ctx)); return modelBuilder;}
public static DbModelBuilder UseSharedDatabaseSeparateSchema(this DbModelBuilder modelBuilder)
{modelBuilder.Conventions.Remove<MultitenantConvention>();
modelBuilder.Conventions.Add(new SharedDatabaseSeparateSchemaConvention()); return modelBuilder;}
public static DbModelBuilder UseSharedDatabaseSharedSchema(this DbModelBuilder modelBuilder, String discriminatorColumnName = "Tenant")
{modelBuilder.Conventions.Remove<MultitenantConvention>();
modelBuilder.Conventions.Add(new SharedDatabaseSharedSchemaConvention(modelBuilder, discriminatorColumnName)); return modelBuilder;}
}
Usage: just uncomment one of the Use calls.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{ //uncomment one of the following lines //modelBuilder.UseSeparateDatabases(this); //modelBuilder.UseSharedDatabaseSeparateSchema(); //modelBuilder.UseSharedDatabaseSharedSchema(discriminatorColumnName: "Tenant"); base.OnModelCreating(modelBuilder);}