diff --git a/src/EFCore.Design/Migrations/Design/CSharpMigrationsGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpMigrationsGenerator.cs index 0dc7d93a83e..b31350895c0 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpMigrationsGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpMigrationsGenerator.cs @@ -240,12 +240,14 @@ public override string GenerateMetadata( /// The model snapshot's type. /// The model snapshot's name. /// The model. + /// The ID of the latest migration that has been applied to the model. /// The model snapshot code. public override string GenerateSnapshot( string? modelSnapshotNamespace, Type contextType, string modelSnapshotName, - IModel model) + IModel model, + string? latestMigrationId = null) { var builder = new IndentedStringBuilder(); AppendAutoGeneratedTag(builder); @@ -288,6 +290,16 @@ public override string GenerateSnapshot( .AppendLine("{"); using (builder.Indent()) { + if (!string.IsNullOrEmpty(latestMigrationId)) + { + builder + .AppendLine("// If you encounter a merge conflict in the line below, it means you need to") + .AppendLine("// discard one of the migration branches and recreate its migrations on top of") + .AppendLine("// the other branch. See https://aka.ms/efcore-docs-migrations-conflicts for more info.") + .Append("public override string LatestMigrationId => ").Append(Code.Literal(latestMigrationId)).AppendLine(";") + .AppendLine(); + } + builder .AppendLine("protected override void BuildModel(ModelBuilder modelBuilder)") .AppendLine("{") diff --git a/src/EFCore.Design/Migrations/Design/IMigrationsCodeGenerator.cs b/src/EFCore.Design/Migrations/Design/IMigrationsCodeGenerator.cs index 281a288204e..9b37b35907d 100644 --- a/src/EFCore.Design/Migrations/Design/IMigrationsCodeGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/IMigrationsCodeGenerator.cs @@ -49,12 +49,14 @@ string GenerateMigration( /// The model snapshot's type. /// The model snapshot's name. /// The model. + /// The ID of the latest migration that has been applied to the model. /// The model snapshot code. string GenerateSnapshot( string? modelSnapshotNamespace, Type contextType, string modelSnapshotName, - IModel model); + IModel model, + string? latestMigrationId = null); /// /// Gets the file extension code files should use. diff --git a/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs b/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs index 16c7dbb5917..fd50ed051a5 100644 --- a/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/MigrationsCodeGenerator.cs @@ -76,12 +76,14 @@ public abstract string GenerateMetadata( /// The model snapshot's type. /// The model snapshot's name. /// The model. + /// The ID of the latest migration that has been applied to the model. /// The model snapshot code. public abstract string GenerateSnapshot( string? modelSnapshotNamespace, Type contextType, string modelSnapshotName, - IModel model); + IModel model, + string? latestMigrationId = null); /// /// Gets the namespaces required for a list of objects. diff --git a/src/EFCore.Design/Migrations/Design/MigrationsScaffolder.cs b/src/EFCore.Design/Migrations/Design/MigrationsScaffolder.cs index ac60a7ff67e..7698372b3a1 100644 --- a/src/EFCore.Design/Migrations/Design/MigrationsScaffolder.cs +++ b/src/EFCore.Design/Migrations/Design/MigrationsScaffolder.cs @@ -187,7 +187,8 @@ public virtual ScaffoldedMigration ScaffoldMigration( modelSnapshotNamespace, _contextType, modelSnapshotName, - Dependencies.Model); + Dependencies.Model, + migrationId); return new ScaffoldedMigration( codeGenerator.FileExtension, @@ -346,6 +347,8 @@ public virtual MigrationFiles RemoveMigration( } } + var latestMigrationId = migrations.Count > 1 ? migrations[^2].GetId() : null; + var modelSnapshotName = modelSnapshot.GetType().Name; var modelSnapshotFileName = modelSnapshotName + codeGenerator.FileExtension; var modelSnapshotFile = TryGetProjectFile(projectDir, modelSnapshotFileName); @@ -378,7 +381,8 @@ public virtual MigrationFiles RemoveMigration( modelSnapshotNamespace, _contextType, modelSnapshotName, - model); + model, + latestMigrationId); modelSnapshotFile ??= Path.Combine( GetDirectory(projectDir, null, GetSubNamespace(rootNamespace, modelSnapshotNamespace)), diff --git a/src/EFCore.Relational/Infrastructure/ModelSnapshot.cs b/src/EFCore.Relational/Infrastructure/ModelSnapshot.cs index 0e13a4b4330..d98421b7c9e 100644 --- a/src/EFCore.Relational/Infrastructure/ModelSnapshot.cs +++ b/src/EFCore.Relational/Infrastructure/ModelSnapshot.cs @@ -28,6 +28,15 @@ private IModel CreateModel() public virtual IModel Model => _model ??= CreateModel(); + /// + /// The ID of the latest migration applied to the model when the snapshot was created. + /// + /// + /// See Database migrations for more information and examples. + /// + public virtual string? LatestMigrationId + => null; + /// /// Called lazily by to build the model snapshot /// the first time it is requested. diff --git a/test/EFCore.Design.Tests/Design/OperationExecutorTest.cs b/test/EFCore.Design.Tests/Design/OperationExecutorTest.cs index 3965bf9a994..7e185a7610b 100644 --- a/test/EFCore.Design.Tests/Design/OperationExecutorTest.cs +++ b/test/EFCore.Design.Tests/Design/OperationExecutorTest.cs @@ -180,6 +180,11 @@ namespace My.Gnomespace.Data [DbContext(typeof(OperationExecutorTest.GnomeContext))] partial class GnomeContextModelSnapshot : ModelSnapshot { + // If you encounter a merge conflict in the line below, it means you need to + // discard one of the migration branches and recreate its migrations on top of + // the other branch. See https://aka.ms/efcore-docs-migrations-conflicts for more info. + public override string LatestMigrationId => "11112233445566_{{migrationName}}"; + protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs index d4ac447c79f..13e0cf78c93 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs @@ -122,6 +122,63 @@ protected override void BuildModel(ModelBuilder modelBuilder) Assert.Equal(2, snapshot.Model.GetEntityTypes().Count()); } + [ConditionalFact] + public void Snapshot_with_migration_id() + { + var generator = CreateMigrationsCodeGenerator(); + + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); + modelBuilder.Entity(x => + { + x.Property(e => e.Id); + }); + + var finalizedModel = modelBuilder.FinalizeModel(designTime: true); + + var modelSnapshotCode = generator.GenerateSnapshot( + "MyNamespace", + typeof(MyContext), + "MySnapshot", + finalizedModel, + "20240101120000_InitialCreate"); + + Assert.Contains("public override string LatestMigrationId => \"20240101120000_InitialCreate\";", modelSnapshotCode); + Assert.Contains("// If you encounter a merge conflict in the line below, it means you need to", modelSnapshotCode); + Assert.Contains("// discard one of the migration branches and recreate its migrations on top of", modelSnapshotCode); + Assert.Contains("// the other branch. See https://aka.ms/efcore-docs-migrations-conflicts for more info.", modelSnapshotCode); + + var snapshot = CompileModelSnapshot(modelSnapshotCode, "MyNamespace.MySnapshot", typeof(MyContext)); + Assert.NotNull(snapshot.Model); + Assert.Equal("20240101120000_InitialCreate", snapshot.LatestMigrationId); + } + + [ConditionalFact] + public void Snapshot_without_migration_id() + { + var generator = CreateMigrationsCodeGenerator(); + + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); + modelBuilder.Entity(x => + { + x.Property(e => e.Id); + }); + + var finalizedModel = modelBuilder.FinalizeModel(designTime: true); + + var modelSnapshotCode = generator.GenerateSnapshot( + "MyNamespace", + typeof(MyContext), + "MySnapshot", + finalizedModel); + + Assert.DoesNotContain("LatestMigrationId", modelSnapshotCode); + Assert.DoesNotContain("merge conflict", modelSnapshotCode); + + var snapshot = CompileModelSnapshot(modelSnapshotCode, "MyNamespace.MySnapshot", typeof(MyContext)); + Assert.NotNull(snapshot.Model); + Assert.Null(snapshot.LatestMigrationId); + } + [ConditionalFact] public void Snapshot_default_values_are_round_tripped() { diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/MigrationTests.cs b/test/EFCore.SqlServer.HierarchyId.Tests/MigrationTests.cs index fdf30e40676..8513756aa09 100644 --- a/test/EFCore.SqlServer.HierarchyId.Tests/MigrationTests.cs +++ b/test/EFCore.SqlServer.HierarchyId.Tests/MigrationTests.cs @@ -15,7 +15,7 @@ public class MigrationTests { private delegate string MigrationCodeGetter(string migrationName, string rootNamespace); - private delegate string SnapshotCodeGetter(string rootNamespace); + private delegate string SnapshotCodeGetter(string rootNamespace, string migrationId); [ConditionalFact] public void Migration_and_snapshot_generate_with_typed_array() @@ -39,9 +39,6 @@ private static void ValidateMigrationAndSnapshotCode( const string migrationName = "MyMigration"; const string rootNamespace = "MyApp.Data"; - var expectedMigration = migrationCodeGetter(migrationName, rootNamespace); - var expectedSnapshot = snapshotCodeGetter(rootNamespace); - var reporter = new OperationReporter( new OperationReportHandler( m => Console.WriteLine($" error: {m}"), @@ -59,6 +56,9 @@ private static void ValidateMigrationAndSnapshotCode( .GetRequiredService() .ScaffoldMigration(migrationName, rootNamespace); + var expectedMigration = migrationCodeGetter(migrationName, rootNamespace); + var expectedSnapshot = snapshotCodeGetter(rootNamespace, migration.MigrationId); + Assert.Equal(expectedMigration, migration.MigrationCode, ignoreLineEndingDifferences: true); Assert.Equal(expectedSnapshot, migration.SnapshotCode, ignoreLineEndingDifferences: true); } diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/TestModels/Migrations/AnonymousArraySeedContext.cs b/test/EFCore.SqlServer.HierarchyId.Tests/TestModels/Migrations/AnonymousArraySeedContext.cs index 9452af5c3cb..174170aa2bb 100644 --- a/test/EFCore.SqlServer.HierarchyId.Tests/TestModels/Migrations/AnonymousArraySeedContext.cs +++ b/test/EFCore.SqlServer.HierarchyId.Tests/TestModels/Migrations/AnonymousArraySeedContext.cs @@ -116,7 +116,7 @@ protected override void Down(MigrationBuilder migrationBuilder) }} "; - public override string GetExpectedSnapshotCode(string rootNamespace) + public override string GetExpectedSnapshotCode(string rootNamespace, string migrationId) => $@"// using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -131,6 +131,11 @@ namespace {rootNamespace}.Migrations [DbContext(typeof({ThisType.Name}))] partial class {ThisType.Name}ModelSnapshot : ModelSnapshot {{ + // If you encounter a merge conflict in the line below, it means you need to + // discard one of the migration branches and recreate its migrations on top of + // the other branch. See https://aka.ms/efcore-docs-migrations-conflicts for more info. + public override string LatestMigrationId => ""{migrationId}""; + protected override void BuildModel(ModelBuilder modelBuilder) {{ #pragma warning disable 612, 618 diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/TestModels/Migrations/MigrationContext.cs b/test/EFCore.SqlServer.HierarchyId.Tests/TestModels/Migrations/MigrationContext.cs index 08d338de11a..c347a9cc590 100644 --- a/test/EFCore.SqlServer.HierarchyId.Tests/TestModels/Migrations/MigrationContext.cs +++ b/test/EFCore.SqlServer.HierarchyId.Tests/TestModels/Migrations/MigrationContext.cs @@ -45,5 +45,5 @@ protected void RemoveVariableModelAnnotations(ModelBuilder modelBuilder) } public abstract string GetExpectedMigrationCode(string migrationName, string rootNamespace); - public abstract string GetExpectedSnapshotCode(string rootNamespace); + public abstract string GetExpectedSnapshotCode(string rootNamespace, string migrationId); } diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/TestModels/Migrations/TypedArraySeedContext.cs b/test/EFCore.SqlServer.HierarchyId.Tests/TestModels/Migrations/TypedArraySeedContext.cs index c54b9ec38b5..94edd2ca7d3 100644 --- a/test/EFCore.SqlServer.HierarchyId.Tests/TestModels/Migrations/TypedArraySeedContext.cs +++ b/test/EFCore.SqlServer.HierarchyId.Tests/TestModels/Migrations/TypedArraySeedContext.cs @@ -116,7 +116,7 @@ protected override void Down(MigrationBuilder migrationBuilder) }} "; - public override string GetExpectedSnapshotCode(string rootNamespace) + public override string GetExpectedSnapshotCode(string rootNamespace, string migrationId) => $@"// using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -131,6 +131,11 @@ namespace {rootNamespace}.Migrations [DbContext(typeof({ThisType.Name}))] partial class {ThisType.Name}ModelSnapshot : ModelSnapshot {{ + // If you encounter a merge conflict in the line below, it means you need to + // discard one of the migration branches and recreate its migrations on top of + // the other branch. See https://aka.ms/efcore-docs-migrations-conflicts for more info. + public override string LatestMigrationId => ""{migrationId}""; + protected override void BuildModel(ModelBuilder modelBuilder) {{ #pragma warning disable 612, 618