Entity Framework Multitenancy
Update: see part two here
Introduction
Multitenancy is currently a hot topic in web development. Azure, SharePoint, and many other frameworks are offering multitenant options, because it totally makes sense to make a better use of a server by hosting many different services.
When it comes to data, there are usually three different strategies:
- Separate databases: each tenant is stored in its own database; different connection strings must be provided, one for each tenant;
- Shared database, separate schemas: all tenant’s data live in the same database and even share table names, but in different schemas;
- Shared database, shared schema tenants share the same physical tables, but use a discriminator column for distinguishing between them.
Let’s explore how we can use each of these techniques in Entity Framework (Code First, of course – is there another?).
Prerequisites
First, we need to have a way to obtain the current tenant, specifically, a tenant id or code. To simplify, let’s just assume a simple interface:
public static class TenantConfiguration
{
public static String GetCurrentTenantId()
{
//doesn't matter
}
}
You are free to implement this in any way you want.
We’ll also have a data context:
public class MultitenantContext : DbContext
{
public DbSet<MultitenantEntity> MultitenantEntities { get; set; }
}
Separate Databases
We need to inject the different connection strings through the constructor:
public MultitenantContext() : base(GetConnectionString())
{
}
private static String GetConnectionString()
{
var currentTenant = TenantConfiguration.GetCurrentTenantId();
return ConfigurationManager.ConnectionStrings[currentTenant].ConnectionString;
}
This is a simple strategy that relies on having one connection string per tenant id, but others exist, of course.
Shared Database, Separate Schemas
Another option is to have each tenant in its own schema. For that, we need to leverage the OnModelCreating method for configuring the model:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
var currentTenant = TenantConfiguration.GetCurrentTenantId();
modelBuilder.Types().Configure(x =>
{
x.ToTable(x.ClrType.Name, currentTenant);
});
base.OnModelCreating(modelBuilder);
}
Again, a simple example: each type gets mapped to an equally-named table and to a schema that is identical to the tenant id. Pay attention to this: SQL Server and other RDBMSs allows users to have a default schema, so in theory, if you use integrated security, you may leave out the explicit schema. However, Entity Framework will always include the schema with the database objects, so you have to explicitly configure it, as we have.
Shared Database, Shared Schema
The final option depends on a discriminator column that is not mapped, but contains a different value for each tenant. Again, we need to configure the model accordingly (warning: reflection ahead):
private static void Map<T>(EntityMappingConfiguration<T> cfg) where T : class
{
var currentTenant = TenantConfiguration.GetCurrentTenantId();
cfg.Requires("Tenant").HasValue(currentTenant);
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
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.Static | BindingFlags.NonPublic).MakeGenericMethod(entity);
var delegateType = typeof(Action<>).MakeGenericType(localMethod.GetParameters().First().ParameterType);
var del = Delegate.CreateDelegate(delegateType, localMethod);
mapMethod.Invoke(entityTypeConfiguration, new Object[] { del });
}
base.OnModelCreating(modelBuilder);
}
This code is required because we need to iterate through all mapped entities, and Entity Framework doesn’t expose everything as public APIs. Some parts could be cached for performance or slightly improved, but I leave that as an exercise to you. In the end, all queries to entities will take an additional restriction “WHERE Tenant = @tenant”, where @tenant will take a different value for each tenant and Tenant is a physical column that isn’t mapped, nor does it need to be.
Conclusion
We’ve seen several techniques for mapping multitenant Entity Framework contexts; ultimately, the one we choose will depend on a number of factors. It will mostly be about having more or less isolation of your data.