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