efcore icon indicating copy to clipboard operation
efcore copied to clipboard

How to do a missing migration check in CI

Open maxkoshevoi opened this issue 3 years ago • 26 comments

Ask a question

I have an idea to add "missing migration" check to my CI pipeline. This check should validate that DbSnapshot that is present in a branch matches models in that branch (in another words, it should validate that if I will create a migration, Up and Down would be empty).

Straightforward way of doing this would be to execute dotnet ef migrations add Test and validate that resulting files match "empty migration" ones, but this seems like a hack.

Is there an easier way of doing this? Like dotnet ef migrations --verify-snapshot-up-to-date or something?

Include provider and version information

EF Core version: Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET 6.0 Operating system: Linux (for CI pipeline) IDE: Visual Studio 2022

maxkoshevoi avatar Oct 14 '21 12:10 maxkoshevoi

@maxkoshevoi We discussed this in triage; there isn't an easy way to do this now, but we agree that it would be useful.

Notes from triage:

  • Issue #22105 covers warning for pending migrations and possible API surface to support this
    • With API, a unit test could be added to check there are no pending migrations
    • It would be useful to check for pending migrations in migrations list
  • Using this issue to track command-line experience for asking specifically if there are pending migrations
    • A command that returns zero if there are no pending migrations and non-zero if there are would be useful in the C.I. where the return value is used to stop/continue the process.

ajcvickers avatar Oct 16 '21 08:10 ajcvickers

Hey!

I found workaround, mb it'll be useful for somebody. You can use migration list command with --json parameter.

dotnet ef migrations list --json

It gives you structed list of migrations with applied property. You can use it to find pending migrations

[  
  {
    "id": "20220420211839_migration1",
    "name": "migration1",
    "safeName": "migration1",
    "applied": true
  },
  {
    "id": "20220621122349_migration2",
    "name": "migration2",
    "safeName": "migration2",
    "applied": false
  }
]

PavelStefanov avatar Jun 21 '22 12:06 PavelStefanov

It gives you structed list of migrations with applied property. You can use it to find pending migrations

The issue here is not that migration hasn't been applied, but that it hasn't been created (there're some model changes that are not reflected in DbContextModelSnapshot.cs), so I don't think your solution is applicable here.

maxkoshevoi avatar Jun 21 '22 13:06 maxkoshevoi

It gives you structed list of migrations with applied property. You can use it to find pending migrations

The issue here is not that migration hasn't been applied, but that it hasn't been created (there're some model changes that are not reflected in DbContextModelSnapshot.cs), so I don't think your solution is applicable here.

Oh sorry. I was confused cuz my issue was marked as related.

PavelStefanov avatar Jun 21 '22 13:06 PavelStefanov

@maxkoshevoi You can create a test and use code similar to this:

https://github.com/ErikEJ/EFCorePowerTools/blob/master/src/GUI/efpt30.core/EFCoreMigrationsManager.cs#L145

ErikEJ avatar Jun 21 '22 13:06 ErikEJ

if you are ok with using some internal EF Core this does what you're asking for:

[Fact]
public void Should_not_have_missing_migrations()
{
    var builder = new DesignTimeServicesBuilder(typeof(MyDbContext).Assembly, Assembly.GetExecutingAssembly(), new OperationReporter(null), Array.Empty<string>());
    var provider = builder.Build(GetDbContext());
    var dependencies = provider.GetRequiredService<MigrationsScaffolderDependencies>();
    var modelSnapshot = dependencies.MigrationsAssembly.ModelSnapshot;
    var model = dependencies.SnapshotModelProcessor.Process(modelSnapshot?.Model);
    var relationalModel = model?.GetRelationalModel();

    var hasDifferences = dependencies.MigrationsModelDiffer.HasDifferences(relationalModel, dependencies.Model.GetRelationalModel());

    hasDifferences.Should().BeFalse();
}

aboryczko avatar Jul 04 '22 12:07 aboryczko

@ErikEJ @aboryczko Thank you! This should work as a workaround. I'd still like to keep this issue open until a more straight forward way is added into the EF Core

maxkoshevoi avatar Jul 04 '22 13:07 maxkoshevoi

if you are ok with using some internal EF Core this does what you're asking for:

[Fact]
public void Should_not_have_missing_migrations()
{
    var builder = new DesignTimeServicesBuilder(typeof(MyDbContext).Assembly, Assembly.GetExecutingAssembly(), new OperationReporter(null), Array.Empty<string>());
    var provider = builder.Build(GetDbContext());
    var dependencies = provider.GetRequiredService<MigrationsScaffolderDependencies>();
    var modelSnapshot = dependencies.MigrationsAssembly.ModelSnapshot;
    var model = dependencies.SnapshotModelProcessor.Process(modelSnapshot?.Model);
    var relationalModel = model?.GetRelationalModel();

    var hasDifferences = dependencies.MigrationsModelDiffer.HasDifferences(relationalModel, dependencies.Model.GetRelationalModel());

    hasDifferences.Should().BeFalse();
}

For anyone using this, if the type 'DesignTimeServicesBuilder' can not be found, make sure "Microsoft.EntityFrameworkCore.Design" is not marked as a private asset in your .csproj file. (It is by default)

@aboryczko Thanks btw!

Checking if a migration is missing in your PR pipeline seems like a no-brainer to me, hoping ef core will get native support for this.

Pentadome avatar Sep 12 '22 13:09 Pentadome

  1. am I right in thinking I need to implement GetDbContext, or is that method provided by something else?
  2. if I do need to write it, any pointers on how to do so?

a unit test is a cool way to solve the problem! however, am I right in understanding that I need to implement GetDbContext myself? this is the path I took, and in order to write that method, I need (I think?) to pull a connection string from the environment-appropriate appsettings file (ex: appsettings.Staging.json), both during CI/CD pipeline (which I think I can do using info from this thread: https://github.com/microsoft/vstest/issues/669 though I haven't tried it yet), AND for devs running tests via their IDE (haven't figured out a way to do this, yet). I've got something like this at the moment:

    private MyDbContext GetDbContext()
    {
        var builder = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json", false, false)
            .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", false, false)
            // ^ 🤷???
        ;

        var config = builder.Build();

        var connectionString = config.GetConnectionString("Db")!;

        var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
        optionsBuilder.UseSqlServer(connectionString);

        var context = new MyDbContext(optionsBuilder.Options);

        return context;
    }

(maybe I could figure out how to get an IApplicationEnvironment in here, but grabbing ASPNETCORE_ENVIRONMENT manually felt like it ought to be good enough?)

BenMakesGames avatar Jan 17 '23 19:01 BenMakesGames

@BenMakesGames yes, you need to create the context. I use something more complex to get the context via IServiceProvider in a base test class, but what you have written should work fine. For a unit test this would be enough:

var builder = new DesignTimeServicesBuilder(typeof(MyDbContext).Assembly, Assembly.GetExecutingAssembly(), new OperationReporter(null), Array.Empty<string>());
var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>().UseSqlServer();
var dbContext = new MyDbContext(optionsBuilder.Options);
var provider = builder.Build(dbContext);
...

of course if you use a different Database Provider you should change UseSqlServer() to something else.

aboryczko avatar Jan 17 '23 20:01 aboryczko

after starting to write a response, I think I'm understanding now; let me confirm: you're saying there's no need to connect to a specific database, because all the information needed to determine "is a new migration needed?" can be determined from code alone? that feels right, and I'll test that now.

BenMakesGames avatar Jan 17 '23 20:01 BenMakesGames

Correct.

aboryczko avatar Jan 17 '23 20:01 aboryczko

I've started some work on implementing this but would need sign off from the team before making a PR.

A command that returns zero if there are no pending migrations and non-zero if there are would be useful in the C.I. where the return value is used to stop/continue the process.

Based on @ajcvickers comment it looks like the team was more leaning towards this being it's own command but from a simplicity standpoint it would be easier if it were an optional switch on the migrations add command. The experience for using such a switch would be like:

dotnet ef migrations add Test --fail-on-empty

The weirdness of this usage is that a lot of us want it to fail if a migration with actual operations is created. It's possible to invert that check based on exit code in a CI pipeline but generally it's more annoying. The other way would be to fail on there being actual operations but I personally haven't thought of a name that I like. Things I have thought about: --require-empty, --verify-no-migrations, and the one thrown out in the issue body --verify-snapshot-up-to-date. I'm completely open to suggestions.

If we were to make it it's own command the design I have thought about is:

dotnet ef migrations verify --no-changes

It feels a little extra to have it's own command for this to me since the code for it would essentially need emulate getting to this part of the code like add. I'm open to any design but would love to start a discussion here about it because I'm serious about implementing this if a PR would be welcome.

justindbaur avatar Apr 12 '23 03:04 justindbaur

@justindbaur We discussed this and we don't think it makes sense to have this be part of the add command. It's really not the same concept.

ajcvickers avatar Apr 27 '23 10:04 ajcvickers

@ajcvickers I don't disagree, it would feel a little weird in the add command, it just would have required the least code changes 😆.

Did the team have any thoughts about what a verify-like command might look like?

justindbaur avatar Apr 27 '23 12:04 justindbaur

@bricelam Could you add your thoughts here?

ajcvickers avatar May 03 '23 18:05 ajcvickers

Dupe of #22105

bricelam avatar May 04 '23 16:05 bricelam

@bricelam can warning fail CI pipeline?

maxkoshevoi avatar May 04 '23 17:05 maxkoshevoi

Most people I've seen write a unit test:

[Fact]
public void No_pending_model_changes()
{
    using var context = new MyDbContext();

    var modelDiffer = context.GetService<IMigrationsModelDiffer>();

    var migrationsAssembly = context.GetService<IMigrationsAssembly>();
    var modelInitializer = context.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 = context.GetService<IDesignTimeModel>();

    var pendingModelChanges = modelDiffer.HasDifferences(
        snapshotModel?.GetRelationalModel(),
        designTimeModel.Model.GetRelationalModel())

    Assert.Empty(pendingModelChanges);
}

#22105 is also about adding a simpler API (something like context.Database.HasPendingModelChanges()) to do this.

bricelam avatar May 04 '23 17:05 bricelam

Note from triage. This issue is still tracking having a command-line experience for asking specifically if there are pending migrations A command that returns zero if there are no pending migrations and non-zero if there are would be useful in the C.I. where the return value is used to stop/continue the process.

This command would be something like dotnet ef migrations check-pending.

ajcvickers avatar May 17 '23 10:05 ajcvickers

just wanted to pop in to give a warning: after updating to the latest minor version of EF 7 (we went from 7.0.2 to 7.0.8), @aboryczko's solution stopped working for me! happily, @bricelam's code works!

I only needed some small tweaks to create an options builder (we're using DateOnly in some of our entities, which requires the ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly package until EF 8, but I assume that's not a super-common use-case).

BenMakesGames avatar Jun 29 '23 19:06 BenMakesGames

I've created a PR adding the check-pending command if people subscribed here have any notes! #31164

justindbaur avatar Jun 30 '23 17:06 justindbaur

Looks like we started talking about two separate issues here.

React to this comment with 🚀 if you're interested in a command that checks whether there are any migrations that haven't been applied to a database (i.e. if you need to run dotnet ef database update)

React with 👀 if you're interested in a command that checks there are any model changes that haven't yet been added to a migration (i.e. if you need to run dotnet ef migrations add)

React with both if you want both.

bricelam avatar Jul 08 '23 00:07 bricelam

FYI, you can test for unapplied migrations today using grep:

dotnet ef migrations list | grep "(Pending)"

bricelam avatar Jul 10 '23 18:07 bricelam

My GitHub Action to ensure that no new migrations are needed

  migration-test:
    name: Migration Test
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Setup .NET
        uses: actions/setup-dotnet@v1
        with:
          dotnet-version: '6.0.x'

      - name: Install EF Core CLI
        run: dotnet tool install --global dotnet-ef

      - name: Generate Migrations
        run: dotnet ef migrations add ${{ github.sha }} <your-params>

      - name: Check Created Migration File Against Regex
        run: |
            if [[ ! $(find <your-migration-folder> -name "*${{ github.sha }}.cs" | xargs grep -E -zo 'protected\s+override\s+void\s+Up\(.*\)\s*\{\s*\}\s*protected\s+override\s+void\s+Down\(.*\)\s*\{\s*\}') ]]; then
                echo "Unapplied Migration Found. Make sure you have run 'dotnet ef migration add ... and committed the generated migration file. See https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli for more information."
                exit 1
            fi

phatt-millips avatar Jul 12 '23 13:07 phatt-millips

Our team doesn't necessarily need something like dotnet ef migrations list | grep "(Pending)" because we always apply pending migrations in the deployment process. However, we have left the add migration step up to the developer so they have to commit the migration, designer, and updated snapshot files

phatt-millips avatar Jul 12 '23 17:07 phatt-millips