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