Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DbMigrator does not detect pending migrations after switching database

EntityFramework migrations become useless after switching to new Context. DbMigrator is using list of Pending Migrations from first database instance, which makes means no migrations are applied to other databases, which then leads to errors during Seed();

  • C# .NET 4.5 MVC project with EF 6
  • MS SQL Server 2014, multiple instances of same database model.
  • CodeFirst approach with migrations.
  • DbContext initializer is set to null.

On Application Start we have custom Db Initialization to create and update databases. CreateDatabaseIfNotExists is working as intended, new databases have all migrations applied. However both MigrateDatabaseToLatestVersion initializer and our custom one are failing to update databases other than first one on list.

foreach (var connectionString in connectionStrings)
{
    using (var context = new ApplicationDbContext(connectionString))
    {
        //Create database
        var created = context.Database.CreateIfNotExists();

        var conf = new Workshop.Migrations.Configuration();
        var migrator = new DbMigrator(conf);

        migrator.Update();

        //initial values
        conf.RunSeed(context);
    }
}
  • context.Database.CreateIfNotExists(); works correctly.
  • migrator.GetLocalMigrations() is always returning correct values.
  • migrator.GetPendingMigrations() after first database is returning empty list.
  • migrator.GetDatabaseMigrations() is mirror of pending migrations, after first database it contains full list event for empty databases.
  • Fetching data (context.xxx.ToList()) from Db instance confirms connection is up and working, and links to correct instance.

Forcing update to most recent migration with migrator.Update("migration_name"); changes nothing. From what I gather by reading EF source code, it checks pending migration list on its own, which gives it faulty results.

There seems to be some caching going in under the hood, but it eludes me how to reset it.

Is there a way to perform migrations on multiple databases or is it yet another "bug by design" in EF?


Edit:

Real problem is DbMigrator creating new Context for its own use. It does it via default parameterless constructor, which in my case had fallback to default (first) connection string in web.Config.

I do not see good solution for this problem but primitive workaround in my case is to temporarily edit default connection string:

var originalConStr = WebConfigurationManager.ConnectionStrings["ApplicationDbContext"].ConnectionString;

var setting = WebConfigurationManager.ConnectionStrings["ApplicationDbContext"];

var fi = typeof(ConfigurationElement).GetField("_bReadOnly", BindingFlags.Instance | BindingFlags.NonPublic);

//disable readonly flag on field
fi.SetValue(setting, false);

setting.ConnectionString = temporaryConnectionString; //now it works

//DO STUFF

setting.ConnectionString = originalConStr; //revert changes

Cheat from: How do I set a connection string config programatically in .net?


I still hope someone will find real solution so for now I will refrain with self-answer.

like image 459
PTwr Avatar asked Mar 17 '16 12:03

PTwr


1 Answers

You need to correctly set DbMigrationsConfiguration.TargetDatabase property, otherwise the migrator will use the default connection info.

So in theory you can do something like this

conf.TargetDatabase = new System.Data.Entity.Infrastructure.DbConnectionInfo(...);

Unfortunately the only 2 public constructors of the DbConnectionInfo are

public DbConnectionInfo(string connectionName)

connectionName: The name of the connection string in the application configuration.

and

public DbConnectionInfo(string connectionString, string providerInvariantName)

connectionString: The connection string to use for the connection.
providerInvariantName: The name of the provider to use for the connection. Use 'System.Data.SqlClient' for SQL Server.

I see you have the connection string, but have no idea how you can get the providerInvariantName.

UPDATE: I didn't find a good "official" way of taking the needed information, so I've ended using a hack with accessing internal members via reflection, but still IMO it's a quite more safer than what you have used:

var internalContext = context.GetType().GetProperty("InternalContext", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(context);
var providerName = (string)internalContext.GetType().GetProperty("ProviderName").GetValue(internalContext);
var conf = new Workshop.Migrations.Configuration();
conf.TargetDatabase = new System.Data.Entity.Infrastructure.DbConnectionInfo(connectionString, providerName);
like image 146
Ivan Stoev Avatar answered Nov 06 '22 20:11

Ivan Stoev