Skip to content

Targeted fix for #35162 - Regression from EF Core 8 to 9: MigrationBuilder.DropTable Causes Issues with Subsequent Table Recreation #35764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2577,10 +2577,16 @@ private IReadOnlyList<MigrationOperation> RewriteOperations(
var newRawSchema = renameTableOperation.NewSchema;
var newSchema = newRawSchema ?? model?.GetDefaultSchema();

var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, renameTableOperation);
if (!temporalTableInformationMap.ContainsKey((tableName, rawSchema)))
{
var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, renameTableOperation);
temporalTableInformationMap[(tableName, rawSchema)] = temporalTableInformation;
}

// we still need to check here - table with the new name could have existed before and have been deleted
// we want to preserve the original temporal info of that deleted table
if (!temporalTableInformationMap.ContainsKey((newTableName, newRawSchema)))
{
temporalTableInformationMap[(newTableName, newRawSchema)] = temporalTableInformation;
}

Expand Down Expand Up @@ -2675,10 +2681,19 @@ private IReadOnlyList<MigrationOperation> RewriteOperations(

var schema = rawSchema ?? model?.GetDefaultSchema();

// we are guaranteed to find entry here - we looped through all the operations earlier,
// info missing from operations we got from the model
// and in case of no/incomplete model we created dummy (non-temporal) entries
var temporalInformation = temporalTableInformationMap[(tableName, rawSchema)];
TemporalOperationInformation temporalInformation;
if (operation is CreateTableOperation)
{
// for create table we always generate new temporal information from the operation itself
// just in case there was a table with that name before that got deleted/renamed
// also, temporal state (disabled versioning etc.) should always reset when creating a table
temporalInformation = BuildTemporalInformationFromMigrationOperation(schema, operation);
temporalTableInformationMap[(tableName, rawSchema)] = temporalInformation;
}
else
{
temporalInformation = temporalTableInformationMap[(tableName, rawSchema)];
}

switch (operation)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3256,6 +3256,118 @@ public virtual Task Add_required_primitve_collection_with_custom_converter_and_c
Assert.Single(customersTable.PrimaryKey!.Columns));
});

[ConditionalFact]
public virtual Task Multiop_drop_table_and_create_the_same_table_in_one_migration()
=> TestComposite(
[
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.Property<string>("Name");
e.HasKey("Id");
e.ToTable("Customers");
}),
builder => { },
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.Property<string>("Name");
e.HasKey("Id");

e.ToTable("Customers");
})
]);

[ConditionalFact]
public virtual Task Multiop_create_table_and_drop_it_in_one_migration()
=> TestComposite(
[
builder => { },
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.Property<string>("Name");
e.HasKey("Id");

e.ToTable("Customers");
}),
builder => { },
]);

[ConditionalFact]
public virtual Task Multiop_rename_table_and_drop()
=> TestComposite(
[
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.Property<string>("Name");
e.HasKey("Id");

e.ToTable("Customers");
}),
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.Property<string>("Name");
e.HasKey("Id");

e.ToTable("NewCustomers");
}),
builder => { },
]);

[ConditionalFact]
public virtual Task Multiop_rename_table_and_create_new_table_with_the_old_name()
=> TestComposite(
[
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.Property<string>("Name");
e.HasKey("Id");

e.ToTable("Customers");
}),
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.Property<string>("Name");
e.HasKey("Id");

e.ToTable("NewCustomers");
}),
builder =>
{
builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.Property<string>("Name");
e.HasKey("Id");

e.ToTable("NewCustomers");
});

builder.Entity(
"AnotherCustomer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.Property<string>("Name");
e.HasKey("Id");

e.ToTable("Customers");
});
},
]);

protected class Person
{
public int Id { get; set; }
Expand Down Expand Up @@ -3305,6 +3417,43 @@ protected virtual Task Test(
MigrationsSqlGenerationOptions migrationsSqlGenerationOptions = MigrationsSqlGenerationOptions.Default)
=> Test(_ => { }, buildSourceAction, buildTargetAction, asserter, withConventions, migrationsSqlGenerationOptions);

protected virtual Task TestComposite(
List<Action<ModelBuilder>> buildActions,
bool withConventions = true,
MigrationsSqlGenerationOptions migrationsSqlGenerationOptions = MigrationsSqlGenerationOptions.Default)
{
if (buildActions.Count < 3)
{
throw new InvalidOperationException("You need at least 3 build actions for the composite case.");
}

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

var models = new List<IModel>();
for (var i = 0; i < buildActions.Count; i++)
{
var modelBuilder = CreateModelBuilder(withConventions);
buildActions[i](modelBuilder);

var preSnapshotModel = modelRuntimeInitializer.Initialize(
(IModel)modelBuilder.Model, designTime: true, validationLogger: null);

models.Add(preSnapshotModel);
}

// build all migration operations going through each intermediate state of the model
var operations = new List<MigrationOperation>();
for (var i = 0; i < models.Count - 1; i++)
{
operations.AddRange(
modelDiffer.GetDifferences(models[i].GetRelationalModel(), models[i + 1].GetRelationalModel()));
}

return Test(models.First(), models.Last(), operations, null, migrationsSqlGenerationOptions);
}

protected virtual Task Test(
Action<ModelBuilder> buildCommonAction,
Action<ModelBuilder> buildSourceAction,
Expand Down
Loading