Mews is used by hotels all over the world in various time zones leaving us with no room for maintenance downtime. This fact, and our aim for continuous deployment left us no other choice than zero downtime deployments, as having even short outages is obviously not an option.
Our application uses Entity Framework with code first approach, meaning that our primary focus is on the code and Entity Framework takes care of mapping entities to tables, creating the schema etc. The problem is that it’s schema migrations executed within the deployment break the old version. Let’s consider a column rename migration. How can we do that in a non-breaking way with zero downtime? Most blog posts would advise you to add a new column with the desired name and make the code update both columns. Together with some SQL migration, which copies all data to the new column, you would deploy it to production, remove the old one and update the usages to use the new one. That’s a lot of work which often results in legacy names of columns remaining in the application, or a lot of manual work for developers. Neither of these sounded good to us. We wanted to make renaming a property as simple as renaming it in your IDE, as it wasn’t even in the database. And we did.
How? The main idea is to take over control of the migrations and split them into two groups. First one being non-breaking changes, which are compatible with both versions of your app and thereby can be executed immediately. Second, the breaking changes, which are scheduled for execution after the new version is up and running.
Let’s look at another simple example: dropping a column. Obviously deleting it right away would break your old, running, version. You also wouldn’t be able to roll-back in case of unexpected issues. However, keeping it breaks the new one when deployed, because it would insert null into a potentially non-nullable column. So to satisfy both, we alter the column to be nullable within the non-breaking part. Now both versions can run on the same database. The breaking part includes the drop itself. And this can be done with all operations.
Example commands:
- Non-breaking:
ALTER TABLE [dbo].[User] ALTER COLUMN [IsAdmin] [bit] NULL
- Breaking:
ALTER TABLE [dbo].[User] DROP COLUMN [IsAdmin]
How to achieve this with Entity Framework? Fortunately, it allows us to create our own MigrationSqlGenerator
, so let’s do it. You also need to implement
DbContextFactory.
internal sealed class CustomMigrationGenerator : SqlServerMigrationSqlGenerator { private MyCustomMigrations Migrations { get; set; } /// <summary> /// This method is called by EF. /// </summary> public override IEnumerable<MigrationStatement> Generate(IEnumerable<MigrationOperation> migrationOperations, string providerManifestToken) { foreach (var operation in migrationOperations) { if (operation is DropColumnOperation dropColumnOp) { // Custom handling here. // Save result to some private property. Migrations = ...; } ... } } internal MyCustomMigrations Generate(DbMigrationsConfiguration<MewsDbContext> configuration) { var info = new DbContextInfo(typeof(MewsDbContext)); configuration.SetSqlGenerator(info.ConnectionProviderName, this); var migrator = new DbMigrator(configuration); var scriptingDecorator = new MigratorScriptingDecorator(migrator); // This will trigger the first method to be called. var script = scriptingDecorator.ScriptUpdate(sourceMigration: null, targetMigration: null); // Return the property set in first method. return Migrations; } }
This is basically everything required for you to start customizing your migrations. Entity framework provides you with an object called MigrationOperation
, describing what should be done. You process these operations and then store the result in a private property, to be reached by the second method. You may use the MigrationStatements
from Entity Framework and return them directly within the first method, but that approach is not flexible since we want to also control the execution of migrations, like scheduling the breaking migrations.
Now you can extend this and customize every MigrationOperation
like we do. In the beginning I spoke about column rename, which is also possible to automate, even though it requires up to 11 custom commands, including database triggers for synchronization. There is a lot to say about the execution part, but I plan to cover this in another post, so make sure you stay tuned.