Is there a way to programmatically check pending model changes in Entity Framework Core?

Viewed 1338

I am currently in the progress of setting up a team environment for ASP.NET Core WebAPI development, using xUnit for unit tests in combination with GitLab CI. For database communication, we use EF Core.

For EF Core we are going to use Code First Migrations and we are worried that a developer might only update the model and not also create a migration for their model change. Thus, we want our CI to run all migrations that exist in the codebase, compare them with the current state of the code first model and fail when the code first model state is not equal to the state that results from running all the migrations.

Is there a way to do this? I cannot find anything about this in the EF Core documentation.

3 Answers

If you are using EF (core) 5 you'll need a slightly different version (also adapted from @ErikEJ sample code)

    [Fact]
    public void ModelDoesNotContainPendingChanges()
    {
        // Do not use the test database, the SQL Server model provider must be
        // used as that is the model provider that is used for scaffolding migrations.
        using var ctx = new DataContext(
            new DbContextOptionsBuilder<DataContext>()
                .UseNpgsql(DummyConnectionString)
                .Options);

        var modelDiffer = ctx.GetService<IMigrationsModelDiffer>();
        var migrationsAssembly = ctx.GetService<IMigrationsAssembly>();

        var dependencies = ctx.GetService<ProviderConventionSetBuilderDependencies>();
        var relationalDependencies = ctx.GetService<RelationalConventionSetBuilderDependencies>();

        var typeMappingConvention = new TypeMappingConvention(dependencies);
        typeMappingConvention.ProcessModelFinalizing(((IConventionModel)migrationsAssembly.ModelSnapshot.Model).Builder, null);

        var relationalModelConvention = new RelationalModelConvention(dependencies, relationalDependencies);
        var sourceModel = relationalModelConvention.ProcessModelFinalized(migrationsAssembly.ModelSnapshot.Model);

        var finalSourceModel = ((IMutableModel)sourceModel).FinalizeModel().GetRelationalModel();
        var finalTargetModel = ctx.Model.GetRelationalModel();

        var hasDifferences = modelDiffer.HasDifferences(finalSourceModel, finalTargetModel);
        if(hasDifferences)
        {
            var changes = modelDiffer.GetDifferences(finalSourceModel, finalTargetModel);
            Assert.True(false, $"{changes.Count} changes between migrations and model. Debug this test for more details");
        }

        Assert.False( hasDifferences );
    }

For EF Core 6, from @ErikEJ's excellent EF Core Power Tools:

var migrationsAssembly = _ctx.GetService<IMigrationsAssembly>();

var hasDifferences = false;
if (migrationsAssembly.ModelSnapshot != null) {
    var snapshotModel = migrationsAssembly.ModelSnapshot?.Model;

    if (snapshotModel is IMutableModel mutableModel) {
        snapshotModel = mutableModel.FinalizeModel();
    }

    snapshotModel = _ctx.GetService<IModelRuntimeInitializer>().Initialize(snapshotModel);
    hasDifferences = _ctx.GetService<IMigrationsModelDiffer>().HasDifferences(
        snapshotModel.GetRelationalModel(),
        _ctx.GetService<IDesignTimeModel>().Model.GetRelationalModel());
}

https://github.com/ErikEJ/EFCorePowerTools/blob/5a16c37c59be854605f3e81d3131011d96c96704/src/GUI/efpt30.core/EFCoreMigrationsBuilder.cs#L98

Thanks to the example code from @ErikEJ, I was able to write the following test that does exactly what I want:

    using FluentAssertions;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Infrastructure;
    using Microsoft.EntityFrameworkCore.Migrations;
    using Xunit;

    /// <summary>
    /// Contains a test that verifies that the
    /// model does not contain any changes that are not included
    /// in the migrations.
    /// </summary>
    public class NoPendingModelChangesTest
    {
        private static readonly string DummyConnectionString = @"Server=localhost;Database=DoesNotExist;Trusted_Connection=True;";

        /// <summary>
        /// Tests that the current model does not contain any changes
        /// that are not contained in the database migrators.
        /// In other words: tests that the current model state equals the
        /// state that results from all the migrations combined.
        /// </summary>
        [Fact]
        public void ModelDoesNotContainPendingChanges()
        {
            // Do not use the test database, the SQL Server model provider must be
            // used as that is the model provider that is used for scaffolding migrations.
            using var ctx = new MyDatabase(
                new DbContextOptionsBuilder<MyDatabase>()
                    .UseSqlServer(DummyConnectionString)
                    .Options);

            var modelDiffer = ctx.GetService<IMigrationsModelDiffer>();
            var migrationsAssembly = ctx.GetService<IMigrationsAssembly>();

            var pendingModelChanges = modelDiffer
                .GetDifferences(
                    migrationsAssembly.ModelSnapshot?.Model,
                    ctx.Model);

            pendingModelChanges
                .Should()
                .BeEmpty(
                    because:
                        "the current model state should be equal to the state that results from all the migrations combined (try scaffolding a migration)");
        }
    }
Related