
Add EF Core Migration Checks to CI
Have you ever had a deploy succeed but then your website error because the code didn't match the database? With Entity Framework Core using migrations this had started to become a thing of the past. The database model is defined in code and with a setup which checks for migrations needing to be applied, the database was always kept up to date. However this is exactly what has happened twice in the last few months in one of our dev environments, and the reason is AI.
EF Core Migrations are scripts that are generated based on your code models. A developer makes code changes to a model, runs the add migration script and the next time apply migrations is run (either through the app startup or another process), the migrations get applied to the DB.
1dotnet ef migrations add AddBlogCreatedTimestamp
However, if an AI tool like Copilot or Claude is being used by the developer you can run into a scenario where a migration script is attempted to be written by the AI, but isn't quite right. In our case it didn't add the .design file.
Now one solution to this is to update your AGENTS.md file to include instructions specifying that your migrations folder should only be updated via the dotnet ef migrations tools, but it would still be advantageous to have your CI tests detect the error and automatically fail the status checks on your PRs. After all this could happen for many other reasons like the changes being missed from a git commit.
Automatically detect model / migration inconsistencies
Fortunately the migration tools have a command to do this exact thing.
1dotnet ef migrations has-pending-model-changes
The has-pending-model-changes command will compare your migration code with the model and return true if changes exist.
If you already have unit tests in your code this can easily be added as a test.
1using MyApp.Infrastructure;2using Microsoft.EntityFrameworkCore;3using Npgsql;45namespace Tests;67public class HasEfModelChangesTests8{9 [Fact]10 public async Task TestHasEfModelChanges()11 {12 await using var dataSource = CreateDataSource();13 await using var dbContext = CreateDbContext(dataSource);1415 var hasChanges = dbContext.Database.HasPendingModelChanges();1617 Assert.False(hasChanges, "The EF model has pending changes. Add a new migration.");18 }1920 // Mirrors the Program.cs setup so the model matches production. No connection is opened.21 private static NpgsqlDataSource CreateDataSource()22 {23 var builder = new NpgsqlDataSourceBuilder("Host=localhost;Database=model_check");24 builder.EnableDynamicJson();25 builder.UseVector();26 return builder.Build();27 }2829 private static AppDbContext CreateDbContext(NpgsqlDataSource dataSource)30 {31 var options = new DbContextOptionsBuilder<AppDbContext>()32 .UseNpgsql(dataSource, o => o.UseVector())33 .Options;3435 return new AppDbContext(options);36 }37}38
One thing to note in my example is that I am using Npgsql rather than an in memory database. Typically for unit tests I'd be using an in memory db and set the on model creating function to ignore any tables not relevant for the test.
In this case we are testing the whole model schema and my model includes data types such as jsonb and pgvector which aren't supported, so instead npgsql is used. No actual DB is required and the connection string is just a dummy connection string, it's just used to access the model building functionality.