Entity Framework & Resolving Team Migration Conflicts

Entity Framework & Resolving Team Migration Conflicts

Zack Davy
Zack Davy

September 18, 2015

I've been working on a .NET project for the past few months. In this project we're using the Entity Framework, which is an ORM for the .NET ecosystem. It's got a really nice interface to it. The migrations are written in code, the entities are expressed as a class with properties, and those properties can be annotated with different attributes.

One problem we've run into is deciding how to manage distributed migrations. We've come across some issues when two separate stories require database schema changes. It turns out that the Entity Framework has a very specific process when creating and executing migrations. I've pored over documentation and blog posts on the subject, and there are a couple of solutions. I'd like to shed a light on what I see as the most responsible way to manage it, in the simplest way possible. We'll build up the scenario, highlight the problem, and elegantly manage the outcome.

Creating the First Migration

It's relatively easy to get started using the code-first migration mindset. Create an entity.


using System.ComponentModel.DataAnnotations;

namespace BookClub.Entities {
				public class Book {
								[Key]
								public int ID { get; set; }
				}
}

Add a database set for it in the Database context.


using System.Data.Entity;
using Bookclub.Entities;

namespace BookClub.Configs {
				public class Database : DbContext {
								public DbSet<Book> Books { get; set; }
				}
}

Enable the migrations in the package manager console.

 PM> Enable-Migrations
 

Add the migration.

 PM> Add-Migration AddBookWithId
 

Add-Migration is going to build out a migration.

using System;
using System.Data.Entity.Migrations;

namespace BookClub.Migrations
{
				public partial class CreateBookWithId : DbMigration
				{
								public override void Up()
								{
												CreateTable(
																"dbo.Books",
																c => new
																{
																				ID = c.Int(false, true)
																}).PrimaryKey(t => t.ID);
								}

								public override void Down()
								{
												DropTable("dbo.Books");
								}
				}
}

Now update the database to run the new migration.

 PM> Update-Database
 

The database now reflects the entities that have been created.

Divide and Conquer

Working in teams allows work to be parallelized. Working on completely separate features allows more progress to be made than stepping on another's toes in the same feature.

We've got a developer named Jack. He's working on adding a feature that changes the entity.


namespace BookClub.Entities {
				public class Book {
								//...
								public Author Author { get; set; }
				}
}

He adds a migration.

PM> Add-Migration AddAuthorToBook

There's another devleoper named Jill. She's working on another feature that changes the same entity.


namespace BookClub.Entities {
				public class Book {
								//...
								public List<Genre> Genres { get; set; }
				}
}

She also adds a migration.

PM> Add-Migration AddGenresToBook

Jill happens to finish her feature first, so she merges her story into master. When Jack goes to merge into master he sees that he can't merge cleanly, since there are conflicts in the entity. He rebases master, and fixes all of the merge conflicts. When he runs the tests, he is notified that there are migrations to be run.


ERROR: Unable to update database to match the current model because there are
pending changes and automatic migration is disabled. Either write the pending
model changes to a code-based migration or enable automatic migration. Set
DbMigrationsConfiguration.AutomaticMigrationsEnabled to true to enable
automatic migration.

No problem, He just updates the database.

 PM> Update-Database
 

He runs the tests but gets the same warning.


ERROR: Unable to update database to match the current model because there are
pending changes and automatic migration is disabled. Either write the pending
model changes to a code-based migration or enable automatic migration. Set
DbMigrationsConfiguration.AutomaticMigrationsEnabled to true to enable
automatic migration.

The Headache

When a migration is created, some metadata and a snapshot of the current entity will also be created. This is necessary in order for the framework to be able to build smarter scaffolds as the entities change, but it's also a source of confusion. The confusion occurs when a new migration is pulled in after a local migration has been run.

The repetitive warning gives no hint for how to handle the scenario, and running the application results in an InvalidOperationException.

The Solution

Though not intuitive, the answer is a quick three-step process:

1) Roll the database back to the latest migration that was merged in.

 PM> Update-Database -target AddGenresToBook
 

This puts the database in a state just before the newest migration should take place.

2) Re-add the local migration. It is suggested to use the timestamp-qualified migration name, however the semantic name will typically suffice.

 PM> Add-Migration AddAuthorToBook
 

Re-adding the migration at this point won't acutally create a new migration. It will instead update the metadata and model state of the already existing migration to reflect the state we've set the database to.

3) Rerun the migration.

 PM> Update-Database
 

And there you have it. Progress may resume as usual without any complicated git cherry-pick workflows, and no silly empty migrations titled MigrationConflict.