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

}

Published by

Ricardo Peres

Team Leader at Dixons Carphone. Microsoft MVP.

Leave a Reply

Your email address will not be published. Required fields are marked *