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);
}