Scenario

We want the Hamburger enumeration class to always be up to date in our database. We configure our DbContext like this:

public class MyDbContext : DbContext
{
    public DbSet<Hamburger> MyEntities { get; set; } = null!;
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Hamburger>(e =>
        {
            e.HasKey(t => t.Value);
            e.Property(t => t.DisplayName).IsRequired();
            e.HasData(Hamburger.GetAll());
        });
    }
}
public partial record Hamburger : IEnumeration<Hamburger>
{
    public static readonly Hamburger Cheeseburger = new (1, "Cheeseburger");
    public static readonly Hamburger BigMac = new(2, "Big Mac");
}

As you can see, we are using HasData to ensure that all the items gets added to the database. If we now generate a new migration, it will look like this:

..........
migrationBuilder.CreateTable(
    name: "hamburgers",
    columns: table => new
    {
        value = table.Column<int>(type: "integer", nullable: false)
                     .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
        display_name = table.Column<string>(type: "text", nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("pk_hamburgers", x => x.value);
    });

migrationBuilder.InsertData(
    table: "hamburgers",
    columns: new[] { "value", "display_name" },
    values: new object[,]
    {
        { 1, "Cheeseburger" },
        { 2, "Big Mac" },
        { 3, "Big Tasty" }
    });
..........

Three rows will be inserted to the database when running this migration, nice.

The problem

Now, what happens when I add another hamburger to my enumeration class?

public partial record Hamburger : IEnumeration<Hamburger>
{
    public static readonly Hamburger Cheeseburger = new (1, "Cheeseburger");
    public static readonly Hamburger BigMac = new(2, "Big Mac");
    public static readonly Hamburger BigTasty = new(3, "Big Tasty");
    public static readonly Hamburger HappyMeal = new(4, "Happy Meal");
}

Nothing happens! And that's to be expected. I haven't created any new migrations, I forgot.

And that's the problem we'll solve today. How can we help ourselves to remember to add new migrations when we've changed our entities?

The solution

We will write a test. The solution is inspired by this comment by bricelam over at GitHub.

[Fact]
public void ModelSnapshotIsInSync()
{
    using var dbContext = new MyDbContext();
    var modelDiffer = dbContext.GetService<IMigrationsModelDiffer>();
    var migrationsAssembly = dbContext.GetService<IMigrationsAssembly>();
    var modelInitializer = dbContext.GetService<IModelRuntimeInitializer>();
    var snapshotModel = migrationsAssembly.ModelSnapshot?.Model;
    if(snapshotModel is IMutableModel mutableModel)
    {
        snapshotModel = mutableModel.FinalizeModel();
    }
    if(snapshotModel is not null)
    {
        snapshotModel = modelInitializer.Initialize(snapshotModel);
    }

    var designTimeModel = dbContext.GetService<IDesignTimeModel>();

    var modelDifferences = modelDiffer.GetDifferences(snapshotModel?.GetRelationalModel(),
        designTimeModel.Model.GetRelationalModel());
    var errorMessage = CreateErrorMessage(modelDifferences);
    modelDifferences.ShouldBeEmpty(errorMessage);
}

private static string CreateErrorMessage(IEnumerable<MigrationOperation> modelDifferences)
{
    var tables = new HashSet<string>();
    foreach(var migrationOperation in modelDifferences)
    {
        switch(migrationOperation)
        {
            case ITableMigrationOperation tableMigrationOperation:
                tables.Add(tableMigrationOperation.Table);
                break;
            default:
                tables.Add(migrationOperation.ToString()!);
                break;
        }
    }

    var stringBuilder = new StringBuilder();
    stringBuilder.AppendLine("The model snapshot is not in sync with the DbContext");
    foreach(var table in tables)
    {
        stringBuilder.AppendLine($"Table '{table}' is not in sync");
    }

    return stringBuilder.ToString();
}

If we run this test, it will fail and print the following:

The model snapshot is not in sync with the DbContext
Table 'hamburgers' is not in sync