5

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.

Community
  • 1
  • 1
PTwr
  • 1,225
  • 1
  • 11
  • 16

1 Answers1

5

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);
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Hmmm... DbContextConfiguration does not want to work as DbMigrationsConfiguration (DbMigrator constructor wants it) so practical approach fails here. Custom connection info fails because DbMigrator ignores it and calls parameterless constructor of my Context which as default uses connection string from web.config. (This is my real problem, it appears). In the end I just edit `ebConfigurationManager.ConnectionStrings["ApplicationDbContext"]` via reflections for duration of update. I hope there is better way tho (singletons/static field does not sound better). – PTwr Mar 17 '16 at 14:26
  • No, I am not. My team leader was ;p – PTwr Mar 17 '16 at 14:30
  • Btw, what is `connectionStrings`? I've noticed you used `.Value`, so it seems to be a some sort of a dictionary? Isn't the key `connectionName`? – Ivan Stoev Mar 17 '16 at 14:52
  • `ConcurrentDictionary`, containing full connection strings fetched from other app. I removed it from sample to avoid confusion :) – PTwr Mar 17 '16 at 15:00
  • Yay, your hack is better :) – PTwr Apr 06 '16 at 12:35
  • +1 Yes, it's a hack. But as long as EF doesn't expose `InternalContext.ProviderName` at least as readonly property you're my hero! – Marcel Jan 31 '17 at 16:30
  • In that first line, what is `db` in the `GetValue(db)` call? Should that be changed to `context` or is it a different object? – Seafish Aug 10 '17 at 18:06
  • @Seafish It should be `context` (copy/paste/modify in SO editor error - I usually call my `DbContext` variables `db` :). Thanks for catching. – Ivan Stoev Aug 10 '17 at 18:18