diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
index b8ff030f3..7d1a7caf3 100644
--- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
+++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
@@ -267,11 +267,26 @@ public virtual NpgsqlOptionsExtension WithEnumMapping(
nameTranslator ??= NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator;
#pragma warning restore CS0618
- clone._enumDefinitions.Add(new EnumDefinition(clrType, enumName, schemaName, nameTranslator));
+ var enumDefinition = new EnumDefinition(clrType, enumName, schemaName, nameTranslator);
+
+ // The same mapping is frequently configured more than once, e.g. when both a context and a context factory are registered
+ // in DI with the same configuration lambda (both lambdas configure the same options). Ignore such duplicates; definitions
+ // which actually differ are kept, and are reported when the mapping is looked up.
+ if (!clone._enumDefinitions.Any(d => IsSameEnumMapping(d, enumDefinition)))
+ {
+ clone._enumDefinitions.Add(enumDefinition);
+ }
return clone;
}
+ private static bool IsSameEnumMapping(EnumDefinition existing, EnumDefinition added)
+ => existing.ClrType == added.ClrType
+ && existing.StoreTypeName == added.StoreTypeName
+ && existing.StoreTypeSchema == added.StoreTypeSchema
+ && existing.Labels.Count == added.Labels.Count
+ && existing.Labels.All(l => added.Labels.TryGetValue(l.Key, out var label) && label == l.Value);
+
///
/// Returns a copy of the current instance configured to use the specified administrative database.
///
diff --git a/test/EFCore.PG.Tests/NpgsqlDbContextOptionsExtensionsTest.cs b/test/EFCore.PG.Tests/NpgsqlDbContextOptionsExtensionsTest.cs
index 27d4446bd..8fef741e9 100644
--- a/test/EFCore.PG.Tests/NpgsqlDbContextOptionsExtensionsTest.cs
+++ b/test/EFCore.PG.Tests/NpgsqlDbContextOptionsExtensionsTest.cs
@@ -145,5 +145,63 @@ public void Varying_data_source_connection_strings_do_not_cause_multiple_service
}
}
+ [ConditionalTheory] // #3375
+ [InlineData(false)]
+ [InlineData(true)]
+ public void MapEnum_configured_twice_by_context_and_factory_registrations(bool pooled)
+ {
+ // Registering both a context and a context factory runs both configuration lambdas against the same options,
+ // so the same MapEnum call is applied twice.
+ var serviceCollection = new ServiceCollection();
+ if (pooled)
+ {
+ serviceCollection.AddDbContextPool(b => b.UseNpgsql("Host=localhost", o => o.MapEnum()));
+ serviceCollection.AddPooledDbContextFactory(b => b.UseNpgsql("Host=localhost", o => o.MapEnum()));
+ }
+ else
+ {
+ serviceCollection.AddDbContext(b => b.UseNpgsql("Host=localhost", o => o.MapEnum()));
+ serviceCollection.AddDbContextFactory(b => b.UseNpgsql("Host=localhost", o => o.MapEnum()));
+ }
+
+ using var services = serviceCollection.BuildServiceProvider();
+
+ using (var scope = services.CreateScope())
+ {
+ AssertMoodMapped(scope.ServiceProvider.GetRequiredService());
+ }
+
+ using var factoryContext = services.GetRequiredService>().CreateDbContext();
+ AssertMoodMapped(factoryContext);
+
+ static void AssertMoodMapped(EnumDbContext context)
+ {
+ var npgsqlOptions = context.GetService().FindExtension()!;
+ var enumDefinition = Assert.Single(npgsqlOptions.EnumDefinitions);
+ Assert.Same(typeof(Mood), enumDefinition.ClrType);
+
+ var property = context.Model.FindEntityType(typeof(Person))!.FindProperty(nameof(Person.Mood))!;
+ Assert.Equal("mood", property.GetRelationalTypeMapping().StoreType);
+ Assert.Equal("mood", Assert.Single(context.GetService().Model.GetPostgresEnums()).Name);
+ }
+ }
+
private class ApplicationDbContext(DbContextOptions options) : DbContext(options);
+
+ private class EnumDbContext(DbContextOptions options) : DbContext(options)
+ {
+ public DbSet People { get; set; }
+ }
+
+ private class Person
+ {
+ public int Id { get; set; }
+ public Mood Mood { get; set; }
+ }
+
+ private enum Mood
+ {
+ Happy,
+ Sad
+ }
}
diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs
index 492e720ae..3a70c9e7e 100644
--- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs
+++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs
@@ -7,6 +7,7 @@
using Npgsql.EntityFrameworkCore.PostgreSQL.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
+using Npgsql.NameTranslation;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage;
@@ -348,7 +349,6 @@ public void Enum_mapping()
}
[Theory]
- [InlineData("dummy_enum", null)]
[InlineData("other_enum", null)]
[InlineData("dummy_enum", "other")]
public void Enum_with_multiple_definitions_for_clr_type(string secondName, string secondSchema)
@@ -361,10 +361,48 @@ public void Enum_with_multiple_definitions_for_clr_type(string secondName, strin
Assert.Throws(() => source.FindMapping(typeof(DummyEnum))).Message);
}
+ [Theory] // #3375
+ [InlineData(null)]
+ [InlineData("some_schema")]
+ public void Enum_mapped_twice_with_same_definition(string schema)
+ {
+ var source = CreateTypeMappingSource(
+ configure: b => b.MapEnum("dummy_enum", schema).MapEnum("dummy_enum", schema));
+ var storeType = schema is null ? "dummy_enum" : schema + ".dummy_enum";
+
+ Assert.Equal(storeType, source.FindMapping(typeof(DummyEnum)).StoreType);
+ Assert.Same(typeof(DummyEnum), source.FindMapping(storeType).ClrType);
+ Assert.Same(typeof(DummyEnum), source.FindMapping(typeof(DummyEnum), storeType).ClrType);
+ }
+
+ [Fact] // #3375
+ public void Enum_mapped_twice_with_equivalent_name_translators()
+ {
+ var source = CreateTypeMappingSource(
+ configure: b => b
+ .MapEnum(nameTranslator: new NpgsqlSnakeCaseNameTranslator())
+ .MapEnum(nameTranslator: new NpgsqlSnakeCaseNameTranslator()));
+
+ Assert.Equal("dummy_enum", source.FindMapping(typeof(DummyEnum)).StoreType);
+ }
+
+ [Fact]
+ public void Enum_mapped_twice_with_different_labels()
+ {
+ // Same CLR type and store type, but the name translators produce different labels ("value" vs. "Value"): this is a real
+ // conflict, so both definitions are kept and the lookup reports it.
+ var source = CreateTypeMappingSource(
+ configure: b => b
+ .MapEnum("dummy_enum", nameTranslator: new NpgsqlSnakeCaseNameTranslator())
+ .MapEnum("dummy_enum", nameTranslator: new NpgsqlNullNameTranslator()));
+
+ Assert.Equal(
+ $"Multiple enum definitions registered for CLR type '{typeof(DummyEnum).FullName}'.",
+ Assert.Throws(() => source.FindMapping(typeof(DummyEnum))).Message);
+ }
+
[Theory]
- [InlineData(null, null, "dummy_enum", typeof(DummyEnum))]
[InlineData(null, null, "dummy_enum", typeof(OtherDummyEnum))]
- [InlineData("test", "test", "test.dummy_enum", typeof(DummyEnum))]
[InlineData("test", "test", "\"test\".\"dummy_enum\"", typeof(OtherDummyEnum))]
[InlineData("one", "two", "dummy_enum", typeof(OtherDummyEnum))]
public void Enum_with_multiple_definitions_for_store_type(