Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -240,12 +240,14 @@ public override string GenerateMetadata(
/// <param name="contextType">The model snapshot's <see cref="DbContext" /> type.</param>
/// <param name="modelSnapshotName">The model snapshot's name.</param>
/// <param name="model">The model.</param>
/// <param name="latestMigrationId">The ID of the latest migration that has been applied to the model.</param>
/// <returns>The model snapshot code.</returns>
public override string GenerateSnapshot(
string? modelSnapshotNamespace,
Type contextType,
string modelSnapshotName,
IModel model)
IModel model,
string? latestMigrationId = null)
{
var builder = new IndentedStringBuilder();
AppendAutoGeneratedTag(builder);
Expand Down Expand Up @@ -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("{")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ string GenerateMigration(
/// <param name="contextType">The model snapshot's <see cref="DbContext" /> type.</param>
/// <param name="modelSnapshotName">The model snapshot's name.</param>
/// <param name="model">The model.</param>
/// <param name="latestMigrationId">The ID of the latest migration that has been applied to the model.</param>
/// <returns>The model snapshot code.</returns>
string GenerateSnapshot(
string? modelSnapshotNamespace,
Type contextType,
string modelSnapshotName,
IModel model);
IModel model,
string? latestMigrationId = null);

/// <summary>
/// Gets the file extension code files should use.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,14 @@ public abstract string GenerateMetadata(
/// <param name="contextType">The model snapshot's <see cref="DbContext" /> type.</param>
/// <param name="modelSnapshotName">The model snapshot's name.</param>
/// <param name="model">The model.</param>
/// <param name="latestMigrationId">The ID of the latest migration that has been applied to the model.</param>
/// <returns>The model snapshot code.</returns>
public abstract string GenerateSnapshot(
string? modelSnapshotNamespace,
Type contextType,
string modelSnapshotName,
IModel model);
IModel model,
string? latestMigrationId = null);

/// <summary>
/// Gets the namespaces required for a list of <see cref="MigrationOperation" /> objects.
Expand Down
8 changes: 6 additions & 2 deletions src/EFCore.Design/Migrations/Design/MigrationsScaffolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ public virtual ScaffoldedMigration ScaffoldMigration(
modelSnapshotNamespace,
_contextType,
modelSnapshotName,
Dependencies.Model);
Dependencies.Model,
migrationId);

return new ScaffoldedMigration(
codeGenerator.FileExtension,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -378,7 +381,8 @@ public virtual MigrationFiles RemoveMigration(
modelSnapshotNamespace,
_contextType,
modelSnapshotName,
model);
model,
latestMigrationId);

modelSnapshotFile ??= Path.Combine(
GetDirectory(projectDir, null, GetSubNamespace(rootNamespace, modelSnapshotNamespace)),
Expand Down
9 changes: 9 additions & 0 deletions src/EFCore.Relational/Infrastructure/ModelSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ private IModel CreateModel()
public virtual IModel Model
=> _model ??= CreateModel();

/// <summary>
/// The ID of the latest migration applied to the model when the snapshot was created.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-migrations-conflicts">Database migrations</see> for more information and examples.
/// </remarks>
public virtual string? LatestMigrationId
=> null;

/// <summary>
/// Called lazily by <see cref="Model" /> to build the model snapshot
/// the first time it is requested.
Expand Down
5 changes: 5 additions & 0 deletions test/EFCore.Design.Tests/Design/OperationExecutorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,63 @@ protected override void BuildModel(ModelBuilder modelBuilder)
Assert.Equal(2, snapshot.Model.GetEntityTypes().Count());
}

[ConditionalFact]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Microsoft.EntityFrameworkCore.Design.OperationExecutorTest.AddMigration_can_scaffold_for_different_names and Microsoft.EntityFrameworkCore.Design.OperationExecutorTest.AddMigration_can_scaffold_for_different_output_dirs are failing because the snapshots haven't been updated:
Assert.Equal() Failure: Strings differ
↓ (pos 414)
Expected: ···"hot\r\n {\r\n protected override vo"···
Actual: ···"hot\r\n {\r\n // If you encounter a"···
↑ (pos 414)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the failing tests. Updated the expected snapshot code in OperationExecutorTest to include the LatestMigrationId property with its explanatory comment. The tests should now pass on Windows. Commit: 8a32ca7

public void Snapshot_with_migration_id()
{
var generator = CreateMigrationsCodeGenerator();

var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder();
modelBuilder.Entity<EntityWithConstructorBinding>(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<EntityWithConstructorBinding>(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()
{
Expand Down
8 changes: 4 additions & 4 deletions test/EFCore.SqlServer.HierarchyId.Tests/MigrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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}"),
Expand All @@ -59,6 +56,9 @@ private static void ValidateMigrationAndSnapshotCode(
.GetRequiredService<IMigrationsScaffolder>()
.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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ protected override void Down(MigrationBuilder migrationBuilder)
}}
";

public override string GetExpectedSnapshotCode(string rootNamespace)
public override string GetExpectedSnapshotCode(string rootNamespace, string migrationId)
=> $@"// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ protected override void Down(MigrationBuilder migrationBuilder)
}}
";

public override string GetExpectedSnapshotCode(string rootNamespace)
public override string GetExpectedSnapshotCode(string rootNamespace, string migrationId)
=> $@"// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
Expand All @@ -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
Expand Down
Loading