TicketDesk 3 Dev Diary – EF Migrations & TD2 Upgrades – Part 3

dead-birdsIn this last installment, I’ll describe TicketDesk’s custom database initializers, which allow TicketDesk 3 to automatically choose which migration should run on startup. I’ll also confess to a major development blunder!

In my last post, I talked about creating the initial migration for new TicketDesk databases. I used the MigrateDatabaseToLatestVersion<T> initializer in global.asax.cs. This initializer works well for normal EF migration usage patterns, but TicketDesk needs initializers with a bit more spunk.

EF also ships with a feature called “automatic migrations”. I will NOT be talking about this feature here. Automatic migrations are, in my opinion, an abortion-in-progress for anything but the most trivial of applications. I advise you to read up on this feature, and then do your best to avoid it. What I will be talking about here is automatically executing a “code-based migration”.

Great primer on Automatic migrations here, and for Code-base migrations here.

Initializers:

A common technique for customizing database initialization is to write a custom initializer that inherits one of the standard initializers. Sadly, the MigrateDatabaseToLatestVersion initializer is ill-suited for inheritance. There are no overridable methods or extension points to plug into. Instead, I’ll roll my own initializers, implementing IDatabaseInitializer<T>, and take direct control over the migrations process. Fortunately, the EF APIs makes this pretty simple.

Initially, I built one big do-it-all initializer, but later switched to separate initializers that operate independently — in keeping with the SRP.

On Application_Start, TicketDesk will run both initializers. Here is how the process should go:

1) Legacy Initializer Runs
    1a) Check if there is a database
    1b) If a database exists, check the version number (stored in the dbo.settings table)
    1c) If version 2.0.2, run the legacy initial migration (else, do nothing)
2) Regular Initializer Runs
    2a) Run all standard migrations

While the initializers don’t need a mediator, or other coordinating object, the order in which they are invoked is relevant to achieve the desired behavior.

The trick to step 2 is that, if the DB was upgraded in step 1c, then the regular migration will think its own initial migration has already run (remember, both use the same ID and hash value). The regular initializer will still run all additional migrations added later, even if the legacy initializer did the initial migration.

The initializers themselves are rather simple. They instantiate EF’s DbMigrator, which is the code equivalent to using the Package Manager Console’s Update-Database command.

The regular initializer:
public class TicketDeskDatabaseInitializer : IDatabaseInitializer<TicketDeskContext>
{
    private string ConnectionName { get; set; }
    public TicketDeskDatabaseInitializer(string connectionName)
    {
        ConnectionName = connectionName;
    }

    public void InitializeDatabase(TicketDeskContext context)
    {
        var config = new Configuration();
        config.TargetDatabase = new DbConnectionInfo(ConnectionName);
        var migrator = new DbMigrator(config);
        migrator.Update(); //run all migrations 
    }
}
The legacy initializer:
public class LegacyDatabaseInitializer : IDatabaseInitializer<TicketDeskLegacyContext>
{

    private string ConnectionName { get; set; }

    public LegacyDatabaseInitializer(string connectionName)
    {
        ConnectionName = connectionName;
    }

    public void InitializeDatabase(TicketDeskLegacyContext context)
    {
        //if existsing TD 2.x database, run upgrade; creates migration history table 
        if (context.Database.Exists() && IsLegacyDatabase(context))
        {
            var upgradeConfig = new Configuration();
            upgradeConfig.TargetDatabase = new DbConnectionInfo(ConnectionName);

            //this will do nothing but add the migration history table 
            //  with the same migration ID as the standard migrator.
            var migrator = new DbMigrator(upgradeConfig);
            migrator.Update("Initial"); //run just the initial migration
        }
    }

    public static bool IsLegacyDatabase(TicketDeskLegacyContext context)
    {
        // TicketDeskLegacyContext has no DbSets, directly execute select query
        var oldVersion = context.Database.SqlQuery<string>(
                  "select SettingValue from Settings where SettingName = 'Version'");
        return 
        (
            oldVersion != null && 
            oldVersion.Count() > 0 && 
            oldVersion.First().Equals("2.0.2")
        );
    }
}

The Accidental Integration Test:

You will note that the constructor for both initializers requires an explicit connection name, which is used to set the target database for the migrator. This is the result of another oversight in how EF migrations were implemented internally –I consider it an oversight anyway.

The initialize method takes an instantiated DbContext as a parameter, but there is no way to pass that context to a DbMigrator. Instead, DbMigrator always creates a new instance of the DbContext, and it always uses the DbContext’s default constructor. So, in cases where you want to use non-default connections, you must explicitly pass that information into the DbMigrator, otherwise it will use whatever connection your DbContext’s default constructor uses.

I discovered this issue when trying unit test the legacy initializer. My unit test used a custom connection string when it instantiated the DbContext (pointing to a localdb file database in the unit test project). I would run the test, but the DB in the test project refused to change.

Eventually, I discovered that the migrator was making a new DbContext from the default constructor, which in this case was hard-coded to a default connection named “TicketDesk”. This connection string was present in my app.config file, but I had, unwisely, left it pointed at my production database server…  the real TicketDesk 2 database that my company’s IT staff uses!

Yikes!

The test was, indeed, migrating the crap out of my production database!

As a testament to the migrator’s effectiveness, and the similarity between the TD3 and TD2 schemas, TicketDesk 2 didn’t skip a beat. The users never knew their DB had been upgraded, and everything kept working. Later, I manually un-migrated the production database using the PM console commands, which also worked without a hitch.

Lesson learned! Again…

Planting Seeds

Another interesting facet to the design of EF Migrations is how it handles seed data. For migrations, seeding is done through an override method in the migration configuration class. The odd thing about this seed method is that it runs every time migrations are run, even if there are no pending migrations to apply to the database. In TicketDesk’s case, both seed methods will run (from legacy and regular configuration classes).

In my opinion, there are two kinds of “seed” data –there is “default data” which is data that should be pre-set when the DB is created, then there is “test data”, which is used for testing or just pre-loading some example data. EF uses “seed data” in the sense of “test data”.

As a practical example of the difference, TicketDesk has a bunch of default settings that are stored in its settings table. These values are required, or the application fails, but admins can change the default settings later if they choose. This is not, in the sense EF uses the term, “seed data”. It is “default data”, for which EF has no explicit support.

For default data, the best solution seems to be inserting the data during the migration itself –by adding insert operations to the migration class’s up and down methods. Unfortunately, there aren’t any convenient helper methods for doing inserts, so you have to issue raw SQL insert commands as strings.

For “test data”, you can rely on the seed method in configuration, as long as you are comfortable with your data being reset every time the application starts. At least you can use Linq helper methods and your strong-typed entities there.

At this point, I have working database initialization and full migrations support in TicketDesk 3, at least for the core model entities. Security, and security database support, may prove more challenging.

Leave a Reply

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