From c18b826c9584221558b0ef1ae2d2fe870ff92e54 Mon Sep 17 00:00:00 2001 From: Brian Pursley Date: Sun, 1 Feb 2026 11:49:50 -0500 Subject: [PATCH 1/2] Fix bug where annotations added by initial migration are not removed when migration is reverted. Adds NpgsqlMigrationsAnnotationProvider to implement ForRemove(IRelationalModel). Fixes #3604 Fixes #3183 Fixes #2514 --- .../NpgsqlServiceCollectionExtensions.cs | 1 + .../Internal/NpgsqlAnnotationHelper.cs | 10 +++++ .../Internal/NpgsqlAnnotationProvider.cs | 7 +--- .../NpgsqlMigrationsAnnotationProvider.cs | 34 +++++++++++++++ .../NpgsqlMigrationsAnnotationProviderTest.cs | 42 +++++++++++++++++++ 5 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationHelper.cs create mode 100644 src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs create mode 100644 test/EFCore.PG.Tests/Migrations/NpgsqlMigrationsAnnotationProviderTest.cs diff --git a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs index 2cd339ca0..fb3ee2192 100644 --- a/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlServiceCollectionExtensions.cs @@ -94,6 +94,7 @@ public static IServiceCollection AddEntityFrameworkNpgsql(this IServiceCollectio .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd() diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationHelper.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationHelper.cs new file mode 100644 index 000000000..7d4690209 --- /dev/null +++ b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationHelper.cs @@ -0,0 +1,10 @@ +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; + +internal static class NpgsqlAnnotationHelper +{ + internal static bool IsRelationalModelAnnotation(IAnnotation annotation) + => annotation.Name.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal) + || annotation.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal) + || annotation.Name.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal) + || annotation.Name.StartsWith(NpgsqlAnnotationNames.CollationDefinitionPrefix, StringComparison.Ordinal); +} diff --git a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs index f92bb721c..fbaf7752f 100644 --- a/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs +++ b/src/EFCore.PG/Metadata/Internal/NpgsqlAnnotationProvider.cs @@ -257,11 +257,6 @@ public override IEnumerable For(IRelationalModel model, bool design return []; } - return model.Model.GetAnnotations().Where( - a => - a.Name.StartsWith(NpgsqlAnnotationNames.PostgresExtensionPrefix, StringComparison.Ordinal) - || a.Name.StartsWith(NpgsqlAnnotationNames.EnumPrefix, StringComparison.Ordinal) - || a.Name.StartsWith(NpgsqlAnnotationNames.RangePrefix, StringComparison.Ordinal) - || a.Name.StartsWith(NpgsqlAnnotationNames.CollationDefinitionPrefix, StringComparison.Ordinal)); + return model.Model.GetAnnotations().Where(NpgsqlAnnotationHelper.IsRelationalModelAnnotation); } } diff --git a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs new file mode 100644 index 000000000..30aeafb91 --- /dev/null +++ b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs @@ -0,0 +1,34 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class NpgsqlMigrationsAnnotationProvider : MigrationsAnnotationProvider +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// +#pragma warning disable EF1001 // Internal EF Core API usage. + public NpgsqlMigrationsAnnotationProvider(MigrationsAnnotationProviderDependencies dependencies) +#pragma warning restore EF1001 + : base(dependencies) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override IEnumerable ForRemove(IRelationalModel model) + => model.Model.GetAnnotations().Where(NpgsqlAnnotationHelper.IsRelationalModelAnnotation); +} diff --git a/test/EFCore.PG.Tests/Migrations/NpgsqlMigrationsAnnotationProviderTest.cs b/test/EFCore.PG.Tests/Migrations/NpgsqlMigrationsAnnotationProviderTest.cs new file mode 100644 index 000000000..2551a1be0 --- /dev/null +++ b/test/EFCore.PG.Tests/Migrations/NpgsqlMigrationsAnnotationProviderTest.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations; + +public class NpgsqlMigrationsAnnotationProviderTest +{ + private readonly NpgsqlMigrationsAnnotationProvider _migrationsAnnotationProvider = new(new MigrationsAnnotationProviderDependencies()); + + [Fact] + public virtual void ForRemove_returns_relational_model_annotations() + { + var modelBuilder = new ModelBuilder() + .HasAnnotation("Npgsql:PostgresExtension:Foo1", "SomeValue") + .HasAnnotation("Npgsql:Enum:Foo2", "SomeValue") + .HasAnnotation("Npgsql:Range:Foo3", "SomeValue") + .HasAnnotation("Npgsql:CollationDefinition:Foo4", "SomeValue") + .HasAnnotation("Npgsql:SomethingElse:Foo5", "SomeValue") // Should be ignored by ForRemove() + .HasAnnotation("Npgsql:Another", "SomeValue") // Should be ignored by ForRemove() + .HasAnnotation("PostgresExtension:Foo1", "SomeValue") // Should be ignored by ForRemove() + .HasAnnotation("Enum:Foo2", "SomeValue") // Should be ignored by ForRemove() + .HasAnnotation("Range:Foo3", "SomeValue") // Should be ignored by ForRemove() + .HasAnnotation("CollationDefinition:Foo4", "SomeValue"); // Should be ignored by ForRemove() + + var model = new RelationalModel(modelBuilder.FinalizeModel()); + + var annotations = _migrationsAnnotationProvider.ForRemove(model).ToList(); + + Assert.Equal(4, annotations.Count); + Assert.Contains(annotations, a => a.Name == "Npgsql:PostgresExtension:Foo1"); + Assert.Contains(annotations, a => a.Name == "Npgsql:Enum:Foo2"); + Assert.Contains(annotations, a => a.Name == "Npgsql:Range:Foo3"); + Assert.Contains(annotations, a => a.Name == "Npgsql:CollationDefinition:Foo4"); + } + + [Fact] + public virtual void ForRemove_handles_no_annotations() + { + var model = new RelationalModel(new Model()); + Assert.Empty(_migrationsAnnotationProvider.ForRemove(model)); + } +} From 9a62ae07d6b5eafd29123639d73511045a0c7867 Mon Sep 17 00:00:00 2001 From: Brian Pursley Date: Sun, 1 Feb 2026 20:37:25 -0500 Subject: [PATCH 2/2] Update unit test to use extension methods to create annotations. --- .../NpgsqlMigrationsAnnotationProviderTest.cs | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/test/EFCore.PG.Tests/Migrations/NpgsqlMigrationsAnnotationProviderTest.cs b/test/EFCore.PG.Tests/Migrations/NpgsqlMigrationsAnnotationProviderTest.cs index 2551a1be0..c0bbdfe86 100644 --- a/test/EFCore.PG.Tests/Migrations/NpgsqlMigrationsAnnotationProviderTest.cs +++ b/test/EFCore.PG.Tests/Migrations/NpgsqlMigrationsAnnotationProviderTest.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Internal; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Migrations; @@ -11,26 +12,33 @@ public class NpgsqlMigrationsAnnotationProviderTest public virtual void ForRemove_returns_relational_model_annotations() { var modelBuilder = new ModelBuilder() - .HasAnnotation("Npgsql:PostgresExtension:Foo1", "SomeValue") - .HasAnnotation("Npgsql:Enum:Foo2", "SomeValue") - .HasAnnotation("Npgsql:Range:Foo3", "SomeValue") - .HasAnnotation("Npgsql:CollationDefinition:Foo4", "SomeValue") - .HasAnnotation("Npgsql:SomethingElse:Foo5", "SomeValue") // Should be ignored by ForRemove() - .HasAnnotation("Npgsql:Another", "SomeValue") // Should be ignored by ForRemove() - .HasAnnotation("PostgresExtension:Foo1", "SomeValue") // Should be ignored by ForRemove() - .HasAnnotation("Enum:Foo2", "SomeValue") // Should be ignored by ForRemove() - .HasAnnotation("Range:Foo3", "SomeValue") // Should be ignored by ForRemove() - .HasAnnotation("CollationDefinition:Foo4", "SomeValue"); // Should be ignored by ForRemove() + .HasPostgresExtension("test_extension") + .HasPostgresExtension("test_schema2", "test_extension2") + .HasPostgresEnum("test_enum", ["A", "B", "C"]) + .HasPostgresEnum("test_schema2", "test_enum2", ["A", "B", "C"]) + .HasPostgresRange("test_range", "test_subtype") + .HasPostgresRange("test_schema2", "test_range2", "test_subtype2") + .HasCollation("test_collation", "test_locale") + .HasCollation("test_schema2", "test_collation2", "test_locale2", "test_provider2", false); + + // Define a sequence, so we can verify that the annotation it creates is excluded from the ForRemove() result. + modelBuilder.HasSequence("test_sequence"); var model = new RelationalModel(modelBuilder.FinalizeModel()); + var allAnnotations = model.Model.GetAnnotations().ToList(); var annotations = _migrationsAnnotationProvider.ForRemove(model).ToList(); - Assert.Equal(4, annotations.Count); - Assert.Contains(annotations, a => a.Name == "Npgsql:PostgresExtension:Foo1"); - Assert.Contains(annotations, a => a.Name == "Npgsql:Enum:Foo2"); - Assert.Contains(annotations, a => a.Name == "Npgsql:Range:Foo3"); - Assert.Contains(annotations, a => a.Name == "Npgsql:CollationDefinition:Foo4"); + Assert.Equal(9, allAnnotations.Count); + Assert.Equal(8, annotations.Count); + Assert.Contains(annotations, a => a.Name == $"{NpgsqlAnnotationNames.PostgresExtensionPrefix}test_extension"); + Assert.Contains(annotations, a => a.Name == $"{NpgsqlAnnotationNames.PostgresExtensionPrefix}test_schema2.test_extension2"); + Assert.Contains(annotations, a => a.Name == $"{NpgsqlAnnotationNames.EnumPrefix}test_enum"); + Assert.Contains(annotations, a => a.Name == $"{NpgsqlAnnotationNames.EnumPrefix}test_schema2.test_enum2"); + Assert.Contains(annotations, a => a.Name == $"{NpgsqlAnnotationNames.RangePrefix}test_range"); + Assert.Contains(annotations, a => a.Name == $"{NpgsqlAnnotationNames.RangePrefix}test_schema2.test_range2"); + Assert.Contains(annotations, a => a.Name == $"{NpgsqlAnnotationNames.CollationDefinitionPrefix}test_collation"); + Assert.Contains(annotations, a => a.Name == $"{NpgsqlAnnotationNames.CollationDefinitionPrefix}test_schema2.test_collation2"); } [Fact]