Monday, May 6, 2013

Entity Framework 5 Code First Multi-Tenancy with Migrations

The Entity Framework 5 Code First Migrations mechanism works great, however there is a small issue regarding multi tenant applications where the same DbContext is used to connect to multiple databases with the same schema.

To be able to address multiple databases with the same schema, you could have two constructors:

public class MyDbContext : DbContext
{
   public MyDbContext() : 
      base( defaultconnectionstring )
   {
   }
 
   public MyDbContext( string ConnectionString ) : 
      base( ConnectionString )
   {
   }
}

or have a single, parameterless constructor but set the connection string after a context has been created. In both cases, the default parameterless constructor will always somehow point to a concrete, default database.

This raises following problem - the MigrateDatabaseToLatestVersion initializer creates your context using the default parameterless dbcontext constructor. It will migrate the database but only the default one! It seems that people report this issue.

To fix this, we need a modified database initializer. The initializer should migrate the current database rather than the default one. It means that the initializer should get the connection string from the current context somehow. Luckily, this is possible:

// an example code first data context
public class MasterDetailContext : DbContext
{
    public DbSet<Detail> Detail { get; set; }
    public DbSet<Master> Master { get; set; }
 
    // this one is used by DbMigrator but the client code will not use it
    public MasterDetailContext()
    {
        Database.Initialize( false );
    }
 
    // rather - the client code will use this one
    public MasterDetailContext( string ConnectionString ) : base( ConnectionString )
    {
        Database.SetInitializer( new CustomInitializer() );
        Database.Initialize( false );
    }
 
    protected override void  OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
    }
}
 
public class CustomInitializer : 
   IDatabaseInitializer<MasterDetailContext>
{
 
    #region IDatabaseInitializer<MasterDetailContext> Members
 
    // fix the problem with MigrateDatabaseToLatestVersion 
    // by copying the connection string FROM the actual context!
    public void InitializeDatabase( MasterDetailContext context )
    {            
        Configuration cfg = new Configuration(); // migration configuration class
        cfg.TargetDatabase = 
           new DbConnectionInfo( 
              context.Database.Connection.ConnectionString, 
              "System.Data.SqlClient" );
 
        DbMigrator dbMigrator = new DbMigrator( cfg );
        // this will call the parameterless constructor of the datacontext
        // but the connection string from above will be then set on in
        dbMigrator.Update();             
    }
 
    #endregion
}

The client code is now:

// Connect to two different schemas using the same context
// Use sql profier to see how both databases are migrated
using ( MasterDetailContext ctx = 
   new MasterDetailContext( @"Database=Database01;Server=.\SQL2012;Integrated Security=true" ) )
{
}
 
using ( MasterDetailContext ctx = 
   new MasterDetailContext( @"Database=Database02;Server=.\SQL2012;Integrated Security=true" ) )
{
}

Compare our new custom initializer with the existing MigrateDatabaseToLatestVersion initializer:

// The builtin initializer class
public class MigrateDatabaseToLatestVersion<TContext, TMigrationsConfiguration> : 
   IDatabaseInitializer<TContext> 
      where TContext : DbContext 
      where TMigrationsConfiguration : DbMigrationsConfiguration<TContext>, new()
{
   private readonly DbMigrationsConfiguration _config;
 
   public MigrateDatabaseToLatestVersion()
   {
      // creates the migrations configuration instance
      this._config = Activator.CreateInstance<TMigrationsConfiguration>();
   }
 
   public void InitializeDatabase(TContext context)
   {
      // migrates the default database (doesn't use context!)
      DbMigrator dbMigrator = new DbMigrator(this._config);
      dbMigrator.Update();
   }   
}

Differences are obvious.

The built in initializer uses reflection to create the instance of the migrations configurator and asks DbMigrator to perform the update but it doesn’t ask it for the connection to the current database! Instead, the configurator by itself creates a new instance of the DbContext using the default parameterless constructor. On the other hand, our custom initializer uses the information on the context to set the configurator’s TargetDatabase to point to a current database.

This way, multiple databases can be migrated independently.

No comments: