From 1128d1264c4ae5f8adf0de770e91b6ef69cb14aa Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 4 Oct 2026 12:49:03 +0200 Subject: [PATCH] RevertMigration: commit both migrations, move to .NET 10, add tests The folder held the model with HasDoors but only the Init migration, so a clean clone crashed on the first insert with "table Hangars has no column named HasDoors". Init is regenerated from the start state and Hangar_HasDoors is added, both keeping the migration IDs the article prints. The regenerated Init also matches the nullable model columns. Retarget to net10.0 with EF Core 10.0.12 and add an xunit test project that applies, reverts and fully reverts the migrations against a temporary SQLite file. --- .../20231209224404_Init.Designer.cs | 16 +-- .../Migrations/20231209224404_Init.cs | 15 ++- ...20231209231131_Hangar_HasDoors.Designer.cs | 82 ++++++++++++++ .../20231209231131_Hangar_HasDoors.cs | 29 +++++ .../AirportDbContextModelSnapshot.cs | 23 ++-- .../MigrationRevertTests.cs | 106 ++++++++++++++++++ .../RevertMigration.Tests.csproj | 32 ++++++ .../RevertMigration/RevertMigration.csproj | 7 +- .../RevertMigration/RevertMigration.sln | 28 ++++- 9 files changed, 304 insertions(+), 34 deletions(-) create mode 100644 dotnet-efcore/RevertMigration/Migrations/20231209231131_Hangar_HasDoors.Designer.cs create mode 100644 dotnet-efcore/RevertMigration/Migrations/20231209231131_Hangar_HasDoors.cs create mode 100644 dotnet-efcore/RevertMigration/RevertMigration.Tests/MigrationRevertTests.cs create mode 100644 dotnet-efcore/RevertMigration/RevertMigration.Tests/RevertMigration.Tests.csproj diff --git a/dotnet-efcore/RevertMigration/Migrations/20231209224404_Init.Designer.cs b/dotnet-efcore/RevertMigration/Migrations/20231209224404_Init.Designer.cs index f537fdb045..2b064621ce 100644 --- a/dotnet-efcore/RevertMigration/Migrations/20231209224404_Init.Designer.cs +++ b/dotnet-efcore/RevertMigration/Migrations/20231209224404_Init.Designer.cs @@ -16,7 +16,7 @@ partial class Init protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.12"); modelBuilder.Entity("Airplane", b => { @@ -24,20 +24,19 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("HangarId") + b.Property("HangarId") .HasColumnType("INTEGER"); - b.Property("MaxAirSpeed") + b.Property("MaxAirSpeed") .HasColumnType("REAL"); - b.Property("NumberOfEngines") + b.Property("NumberOfEngines") .HasColumnType("INTEGER"); - b.Property("RunsOnJetFuel") + b.Property("RunsOnJetFuel") .HasColumnType("INTEGER"); b.Property("TailNumber") - .IsRequired() .HasColumnType("TEXT"); b.HasKey("Id"); @@ -54,7 +53,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER"); b.Property("HangarNumber") - .IsRequired() .HasColumnType("TEXT"); b.HasKey("Id"); @@ -66,9 +64,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.HasOne("Hangar", "Hangar") .WithMany("Airplanes") - .HasForeignKey("HangarId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("HangarId"); b.Navigation("Hangar"); }); diff --git a/dotnet-efcore/RevertMigration/Migrations/20231209224404_Init.cs b/dotnet-efcore/RevertMigration/Migrations/20231209224404_Init.cs index 1a214cb86f..0dd8f05d60 100644 --- a/dotnet-efcore/RevertMigration/Migrations/20231209224404_Init.cs +++ b/dotnet-efcore/RevertMigration/Migrations/20231209224404_Init.cs @@ -16,7 +16,7 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - HangarNumber = table.Column(type: "TEXT", nullable: false) + HangarNumber = table.Column(type: "TEXT", nullable: true) }, constraints: table => { @@ -29,11 +29,11 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - TailNumber = table.Column(type: "TEXT", nullable: false), - NumberOfEngines = table.Column(type: "INTEGER", nullable: false), - MaxAirSpeed = table.Column(type: "REAL", nullable: false), - RunsOnJetFuel = table.Column(type: "INTEGER", nullable: false), - HangarId = table.Column(type: "INTEGER", nullable: false) + TailNumber = table.Column(type: "TEXT", nullable: true), + NumberOfEngines = table.Column(type: "INTEGER", nullable: true), + MaxAirSpeed = table.Column(type: "REAL", nullable: true), + RunsOnJetFuel = table.Column(type: "INTEGER", nullable: true), + HangarId = table.Column(type: "INTEGER", nullable: true) }, constraints: table => { @@ -42,8 +42,7 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "FK_Airplanes_Hangars_HangarId", column: x => x.HangarId, principalTable: "Hangars", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); + principalColumn: "Id"); }); migrationBuilder.CreateIndex( diff --git a/dotnet-efcore/RevertMigration/Migrations/20231209231131_Hangar_HasDoors.Designer.cs b/dotnet-efcore/RevertMigration/Migrations/20231209231131_Hangar_HasDoors.Designer.cs new file mode 100644 index 0000000000..52d148d313 --- /dev/null +++ b/dotnet-efcore/RevertMigration/Migrations/20231209231131_Hangar_HasDoors.Designer.cs @@ -0,0 +1,82 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace RevertMigration.Migrations +{ + [DbContext(typeof(AirportDbContext))] + [Migration("20231209231131_Hangar_HasDoors")] + partial class Hangar_HasDoors + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.12"); + + modelBuilder.Entity("Airplane", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("HangarId") + .HasColumnType("INTEGER"); + + b.Property("MaxAirSpeed") + .HasColumnType("REAL"); + + b.Property("NumberOfEngines") + .HasColumnType("INTEGER"); + + b.Property("RunsOnJetFuel") + .HasColumnType("INTEGER"); + + b.Property("TailNumber") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("HangarId"); + + b.ToTable("Airplanes"); + }); + + modelBuilder.Entity("Hangar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("HangarNumber") + .HasColumnType("TEXT"); + + b.Property("HasDoors") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Hangars"); + }); + + modelBuilder.Entity("Airplane", b => + { + b.HasOne("Hangar", "Hangar") + .WithMany("Airplanes") + .HasForeignKey("HangarId"); + + b.Navigation("Hangar"); + }); + + modelBuilder.Entity("Hangar", b => + { + b.Navigation("Airplanes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/dotnet-efcore/RevertMigration/Migrations/20231209231131_Hangar_HasDoors.cs b/dotnet-efcore/RevertMigration/Migrations/20231209231131_Hangar_HasDoors.cs new file mode 100644 index 0000000000..ec92717e39 --- /dev/null +++ b/dotnet-efcore/RevertMigration/Migrations/20231209231131_Hangar_HasDoors.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RevertMigration.Migrations +{ + /// + public partial class Hangar_HasDoors : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "HasDoors", + table: "Hangars", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "HasDoors", + table: "Hangars"); + } + } +} diff --git a/dotnet-efcore/RevertMigration/Migrations/AirportDbContextModelSnapshot.cs b/dotnet-efcore/RevertMigration/Migrations/AirportDbContextModelSnapshot.cs index aa09408b88..9bb4009fe3 100644 --- a/dotnet-efcore/RevertMigration/Migrations/AirportDbContextModelSnapshot.cs +++ b/dotnet-efcore/RevertMigration/Migrations/AirportDbContextModelSnapshot.cs @@ -13,7 +13,7 @@ partial class AirportDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.12"); modelBuilder.Entity("Airplane", b => { @@ -21,27 +21,26 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("HangarId") + b.Property("HangarId") .HasColumnType("INTEGER"); - b.Property("MaxAirSpeed") + b.Property("MaxAirSpeed") .HasColumnType("REAL"); - b.Property("NumberOfEngines") + b.Property("NumberOfEngines") .HasColumnType("INTEGER"); - b.Property("RunsOnJetFuel") + b.Property("RunsOnJetFuel") .HasColumnType("INTEGER"); b.Property("TailNumber") - .IsRequired() .HasColumnType("TEXT"); b.HasKey("Id"); b.HasIndex("HangarId"); - b.ToTable("Airplanes", (string)null); + b.ToTable("Airplanes"); }); modelBuilder.Entity("Hangar", b => @@ -51,21 +50,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER"); b.Property("HangarNumber") - .IsRequired() .HasColumnType("TEXT"); + b.Property("HasDoors") + .HasColumnType("INTEGER"); + b.HasKey("Id"); - b.ToTable("Hangars", (string)null); + b.ToTable("Hangars"); }); modelBuilder.Entity("Airplane", b => { b.HasOne("Hangar", "Hangar") .WithMany("Airplanes") - .HasForeignKey("HangarId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("HangarId"); b.Navigation("Hangar"); }); diff --git a/dotnet-efcore/RevertMigration/RevertMigration.Tests/MigrationRevertTests.cs b/dotnet-efcore/RevertMigration/RevertMigration.Tests/MigrationRevertTests.cs new file mode 100644 index 0000000000..30b454f52b --- /dev/null +++ b/dotnet-efcore/RevertMigration/RevertMigration.Tests/MigrationRevertTests.cs @@ -0,0 +1,106 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +public class MigrationRevertTests : IDisposable +{ + private const string InitMigration = "20231209224404_Init"; + private const string HasDoorsMigration = "20231209231131_Hangar_HasDoors"; + + private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"airport-{Guid.NewGuid():N}.db"); + + private AirportDbContext CreateContext() + { + // The sample keeps its database in LocalApplicationData; the tests use a throwaway file instead. + var db = new AirportDbContext(); + db.Database.SetConnectionString($"Data Source={_dbPath}"); + + return db; + } + + private static List GetHangarColumns(AirportDbContext db) + { + var connection = db.Database.GetDbConnection(); + connection.Open(); + try + { + using var command = connection.CreateCommand(); + command.CommandText = "SELECT name FROM pragma_table_info('Hangars');"; + using var reader = command.ExecuteReader(); + + var columns = new List(); + while (reader.Read()) + { + columns.Add(reader.GetString(0)); + } + + return columns; + } + finally + { + connection.Close(); + } + } + + [Fact] + public void WhenAllMigrationsAreApplied_ThenHangarsHasTheHasDoorsColumn() + { + using var db = CreateContext(); + + db.Database.Migrate(); + + Assert.Equal([InitMigration, HasDoorsMigration], db.Database.GetAppliedMigrations()); + Assert.Contains("HasDoors", GetHangarColumns(db)); + } + + [Fact] + public void WhenAllMigrationsAreApplied_ThenTheSampleCanSaveAHangarWithDoors() + { + using (var db = CreateContext()) + { + db.Database.Migrate(); + db.Add(new Hangar { HangarNumber = "HANGAR_01", HasDoors = true }); + db.SaveChanges(); + } + + using (var db = CreateContext()) + { + var hangar = db.Hangars.Single(); + + Assert.Equal("HANGAR_01", hangar.HangarNumber); + Assert.True(hangar.HasDoors); + } + } + + [Fact] + public void WhenDatabaseIsUpdatedToInit_ThenTheHasDoorsColumnIsGone() + { + using var db = CreateContext(); + db.Database.Migrate(); + + db.GetService().Migrate(InitMigration); + + Assert.Equal([InitMigration], db.Database.GetAppliedMigrations()); + Assert.Equal([HasDoorsMigration], db.Database.GetPendingMigrations()); + Assert.DoesNotContain("HasDoors", GetHangarColumns(db)); + } + + [Fact] + public void WhenDatabaseIsUpdatedToZero_ThenEveryMigrationIsReverted() + { + using var db = CreateContext(); + db.Database.Migrate(); + + db.GetService().Migrate(Migration.InitialDatabase); + + Assert.Empty(db.Database.GetAppliedMigrations()); + Assert.Empty(GetHangarColumns(db)); + } + + public void Dispose() + { + SqliteConnection.ClearAllPools(); + File.Delete(_dbPath); + } +} diff --git a/dotnet-efcore/RevertMigration/RevertMigration.Tests/RevertMigration.Tests.csproj b/dotnet-efcore/RevertMigration/RevertMigration.Tests/RevertMigration.Tests.csproj new file mode 100644 index 0000000000..d406e3d4eb --- /dev/null +++ b/dotnet-efcore/RevertMigration/RevertMigration.Tests/RevertMigration.Tests.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + enable + enable + false + true + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/dotnet-efcore/RevertMigration/RevertMigration.csproj b/dotnet-efcore/RevertMigration/RevertMigration.csproj index 9392e9abdb..3b8808ec91 100644 --- a/dotnet-efcore/RevertMigration/RevertMigration.csproj +++ b/dotnet-efcore/RevertMigration/RevertMigration.csproj @@ -2,17 +2,18 @@ Exe - net8.0 + net10.0 enable enable + $(DefaultItemExcludes);RevertMigration.Tests/** - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/dotnet-efcore/RevertMigration/RevertMigration.sln b/dotnet-efcore/RevertMigration/RevertMigration.sln index a2a07939b2..9d7ac64b06 100644 --- a/dotnet-efcore/RevertMigration/RevertMigration.sln +++ b/dotnet-efcore/RevertMigration/RevertMigration.sln @@ -1,20 +1,46 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.5.002.0 +VisualStudioVersion = 17.5.2.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RevertMigration", "RevertMigration.csproj", "{C4FB3D79-BCE1-4C08-A791-A32F5B4E24CD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RevertMigration.Tests", "RevertMigration.Tests\RevertMigration.Tests.csproj", "{9AD6860E-FC02-4404-87CC-8B4F71919AA8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {C4FB3D79-BCE1-4C08-A791-A32F5B4E24CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C4FB3D79-BCE1-4C08-A791-A32F5B4E24CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4FB3D79-BCE1-4C08-A791-A32F5B4E24CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {C4FB3D79-BCE1-4C08-A791-A32F5B4E24CD}.Debug|x64.Build.0 = Debug|Any CPU + {C4FB3D79-BCE1-4C08-A791-A32F5B4E24CD}.Debug|x86.ActiveCfg = Debug|Any CPU + {C4FB3D79-BCE1-4C08-A791-A32F5B4E24CD}.Debug|x86.Build.0 = Debug|Any CPU {C4FB3D79-BCE1-4C08-A791-A32F5B4E24CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {C4FB3D79-BCE1-4C08-A791-A32F5B4E24CD}.Release|Any CPU.Build.0 = Release|Any CPU + {C4FB3D79-BCE1-4C08-A791-A32F5B4E24CD}.Release|x64.ActiveCfg = Release|Any CPU + {C4FB3D79-BCE1-4C08-A791-A32F5B4E24CD}.Release|x64.Build.0 = Release|Any CPU + {C4FB3D79-BCE1-4C08-A791-A32F5B4E24CD}.Release|x86.ActiveCfg = Release|Any CPU + {C4FB3D79-BCE1-4C08-A791-A32F5B4E24CD}.Release|x86.Build.0 = Release|Any CPU + {9AD6860E-FC02-4404-87CC-8B4F71919AA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AD6860E-FC02-4404-87CC-8B4F71919AA8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AD6860E-FC02-4404-87CC-8B4F71919AA8}.Debug|x64.ActiveCfg = Debug|Any CPU + {9AD6860E-FC02-4404-87CC-8B4F71919AA8}.Debug|x64.Build.0 = Debug|Any CPU + {9AD6860E-FC02-4404-87CC-8B4F71919AA8}.Debug|x86.ActiveCfg = Debug|Any CPU + {9AD6860E-FC02-4404-87CC-8B4F71919AA8}.Debug|x86.Build.0 = Debug|Any CPU + {9AD6860E-FC02-4404-87CC-8B4F71919AA8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AD6860E-FC02-4404-87CC-8B4F71919AA8}.Release|Any CPU.Build.0 = Release|Any CPU + {9AD6860E-FC02-4404-87CC-8B4F71919AA8}.Release|x64.ActiveCfg = Release|Any CPU + {9AD6860E-FC02-4404-87CC-8B4F71919AA8}.Release|x64.Build.0 = Release|Any CPU + {9AD6860E-FC02-4404-87CC-8B4F71919AA8}.Release|x86.ActiveCfg = Release|Any CPU + {9AD6860E-FC02-4404-87CC-8B4F71919AA8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE