Multiple contexts per db and automatic migration

Topics: EF Runtime
Nov 19, 2013 at 3:55 PM
In my current project I use one db for different contexts using HasDefaultSchema. All contexts have a custom initializer, that automatically migrates to the newest version using Database.CompatibleWithModel(), DbMigrator.Update() and a configuration that has AutomaticMigrationsEnabled set to true.

Two things are unclear to me:

Initial DbMigration

The first Add-Migration for each contexts contains the complete model - as expected. Accessing the contexts for the first time calls the initializer and updates the db correctly using the specified schemas. From this point on, automatic migrations are applied correctly for all contexts/schemas. Without an initial Add-Migration the migrator fails to insert any table, even if the db is empty.

The documentation says:
"However, for implementation reasons, transferring the __MigrationHistory table to a different schema can only occur using a code-based migration."

So, why does migrations refuse to automatically update an empty db without an initial DbMigration, but adds tables of an other context/schema to a db, that already contains tables of another context/schema (with an initial code-based migration)? The expected behavior was, that without any added DbMigration the complete model gets added to an empty db, where no schema transferring should be neccessary.

DbMigration discovery

Not having read the source code of ef much, it's hard to know how DbMigrations are discovered. I implemented a custom initializer to do automatic migration, because my connection string names are only known at runtime (one db per tenant).

When using DbMigrationsConfiguration<TContext> only to enable automatic migrations,the custom initializer could be simplified by just creating a generic configuration for the context at runtime, enable automatic migrations and supply it to the DbMigrator.

If I don't use a derived configuration class in the same assembly as the migrations, the initial DbMigration can not be found (in the assembly of the context) and the update fails. Is there a way to tell DbMigrator where the DbMigrations live without implementing a configuration?

Thanks in advance
mat
Developer
Nov 19, 2013 at 6:10 PM
Hi mat,

I think you might need to take a look (and set the correct values for) the following DbMigrationsConfiguration properties:

1) MigrationsNamespace/MigrationsAssembly - this is how to specify the location of the indivual DbMigrations classes.
2) ContextKey - When using multiple contexts per database, this property is used as a secondary key when accessing the __MigrationHistory table. Normally, the default context key is fine because it is the type name of the derived migrations config. However, if there is no derived config then you will want to explicitly set it to something unique per context. i.e. The type name of the context.

Let me know if this helps.
  • Andrew.
Nov 22, 2013 at 12:20 PM
Hi Andrew,

thanks for your hints. I could get rid of the derived migration configs by setting the correct assembly and namespace. This way the initial migrations for each context are found and applied correctly. That's a first little success!

Without this first initial code-based migration there is always this exception:
Automatic migrations that affect the location of the migrations history system table (such as default schema changes) are not supported. Please use code-based migrations for operations that affect the location of the migrations history system table.

However, this initial DbMigration feels redundant, because the migration process looks like this:
1) No/empty db -> initial code-based and possibly outdated migration is applied
2) The current model has changed since the initial migration -> another automatic migration is created and applied

Is this first migration really neccessary (technically), when you could just apply the current model state directly?
(I also thought about a creating a faked initial code-based migration manually, that has an empty model (similar to http://stackoverflow.com/a/15027630), to avoid having an outdated code-based migration with a few hundred lines of outdated code...)

Thanks for your help
mat

Here is my MigrationInitializer:
    public class MigrationInitializer<TContext>
        : IDatabaseInitializer<TContext>
        where TContext : DbContext, new()
    {
        private readonly string _connectionString;

        public MigrationInitializer(string connectionString)
        {
            _connectionString = connectionString;
        }

        public void InitializeDatabase(TContext context)
        {
            try
            {
                // database already compatible?
                var isCompatible = true;
                try { isCompatible = context.Database.CompatibleWithModel(true); }
                catch (Exception) { isCompatible = false; }
                if (isCompatible)
                    return;

                // apply current connection
                var contextType = typeof(TContext);
                var config = new DbMigrationsConfiguration<TContext> {
                    AutomaticMigrationsEnabled = true,
                    AutomaticMigrationDataLossAllowed = true,
                    ContextKey = contextType.Name,
                    MigrationsAssembly = contextType.Assembly,
                    MigrationsNamespace = contextType.Namespace + ".Migrations",
                    TargetDatabase = new DbConnectionInfo(
                        context.Database.Connection.ConnectionString,
                        "System.Data.SqlClient") };

                // migrate to latest version
                var migrator = new DbMigrator(config);
                migrator.Update();
            }
            catch (Exception) { ... }
        }
    }