From 592295210e32d689e50ea80f062f9178cd4beaa8 Mon Sep 17 00:00:00 2001 From: Rubens Cordeiro Date: Sat, 8 Mar 2025 16:03:10 +0100 Subject: [PATCH 1/2] Improve error message when duplicate enum definitions are registered. --- .../Storage/Internal/NpgsqlTypeMappingSource.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index cd5a08b37..a5e0c05fd 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -865,7 +865,14 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType) EnumDefinition? enumDefinition; if (storeType is null) { - enumDefinition = _enumDefinitions.SingleOrDefault(m => m.ClrType == clrType); + var matches = _enumDefinitions.Where(m => m.ClrType == clrType).ToList(); + if (matches.Count > 1) + { + throw new InvalidOperationException( + $"Multiple enum definitions registered for CLR type '{clrType?.FullName}'. "); + } + + enumDefinition = matches.FirstOrDefault(); if (enumDefinition is null) { From 62765e16c0d4fb8d1fd9c3a3a0254bcf76e970c5 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 20 Oct 2026 12:07:27 +0200 Subject: [PATCH 2/2] Cover duplicate enum diagnostics for store type lookups Preserve enum name and schema matching while reporting ambiguous store type registrations explicitly. Add regression coverage for CLR and store type diagnostics and successful disambiguation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 788de7d6-4106-45dd-b130-93f79e4df03d --- .../Internal/NpgsqlTypeMappingSource.cs | 15 ++- .../Storage/NpgsqlTypeMappingSourceTest.cs | 97 ++++++++++++++++++- 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index a5e0c05fd..75a160954 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -869,7 +869,7 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType) if (matches.Count > 1) { throw new InvalidOperationException( - $"Multiple enum definitions registered for CLR type '{clrType?.FullName}'. "); + $"Multiple enum definitions registered for CLR type '{clrType?.FullName}'."); } enumDefinition = matches.FirstOrDefault(); @@ -890,9 +890,16 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType) // names here, by parsing the name and stripping the quotes. ParseStoreTypeName(storeType, out name, out schema, out var size, out var precision, out var scale); - enumDefinition = schema is null - ? _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == name) - : _enumDefinitions.SingleOrDefault(m => m.StoreTypeName == name && m.StoreTypeSchema == schema); + var matches = _enumDefinitions + .Where(m => m.StoreTypeName == name && (schema is null || m.StoreTypeSchema == schema)) + .ToList(); + if (matches.Count > 1) + { + throw new InvalidOperationException( + $"Multiple enum definitions registered for store type '{storeType}'."); + } + + enumDefinition = matches.FirstOrDefault(); if (enumDefinition is null) { diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs index 118a9bb58..2a8e7a7ea 100644 --- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs +++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingSourceTest.cs @@ -313,6 +313,89 @@ private void Array_over_type_mapping_with_value_converter(CoreTypeMapping mappin #endregion Array + #region Enum + + [Fact] + public void Enum_mapping() + { + var source = CreateTypeMappingSource(configure: b => b.MapEnum("dummy_enum")); + + var mapping = source.FindMapping(typeof(DummyEnum)); + + Assert.IsType(mapping); + Assert.Equal("dummy_enum", mapping.StoreType); + Assert.Same(typeof(DummyEnum), mapping.ClrType); + Assert.Same(typeof(DummyEnum), source.FindMapping("dummy_enum").ClrType); + Assert.Same(typeof(DummyEnum), source.FindMapping(typeof(DummyEnum), "dummy_enum").ClrType); + Assert.Null(source.FindMapping("unknown_enum")); + Assert.Equal("integer", CreateTypeMappingSource().FindMapping(typeof(DummyEnum)).StoreType); + } + + [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) + { + var source = CreateTypeMappingSource( + configure: b => b.MapEnum("dummy_enum").MapEnum(secondName, secondSchema)); + + 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( + string firstSchema, string secondSchema, string storeType, Type secondClrType) + { + var source = CreateTypeMappingSource( + configure: b => b.MapEnum("dummy_enum", firstSchema) + .MapEnum(secondClrType, "dummy_enum", secondSchema)); + var expectedMessage = $"Multiple enum definitions registered for store type '{storeType}'."; + + Assert.Equal( + expectedMessage, + Assert.Throws(() => source.FindMapping(storeType)).Message); + Assert.Equal( + expectedMessage, + Assert.Throws(() => source.FindMapping(typeof(DummyEnum), storeType)).Message); + } + + [Theory] + [InlineData("one.dummy_enum", typeof(DummyEnum))] + [InlineData("two.dummy_enum", typeof(OtherDummyEnum))] + [InlineData("\"one\".\"dummy_enum\"", typeof(DummyEnum))] + public void Enum_mapping_disambiguated_by_schema(string storeType, Type clrType) + { + var source = CreateTypeMappingSource( + configure: b => b.MapEnum("dummy_enum", "one").MapEnum("dummy_enum", "two")); + + Assert.Same(clrType, source.FindMapping(storeType).ClrType); + Assert.Same(clrType, source.FindMapping(clrType, storeType).ClrType); + } + + [Theory] + [InlineData("first_enum")] + [InlineData("second_enum")] + public void Enum_mapping_disambiguated_by_store_type(string storeType) + { + var source = CreateTypeMappingSource( + configure: b => b.MapEnum("first_enum").MapEnum("second_enum")); + + var mapping = source.FindMapping(typeof(DummyEnum), storeType); + + Assert.Same(typeof(DummyEnum), mapping.ClrType); + Assert.Equal(storeType, mapping.StoreType); + } + + #endregion Enum + #region JSON [Fact] @@ -453,7 +536,8 @@ public void ParseStoreType(string storeTypeName, string expectedName, string? ex #region Support - private NpgsqlTypeMappingSource CreateTypeMappingSource(Version postgresVersion = null) + private NpgsqlTypeMappingSource CreateTypeMappingSource( + Version postgresVersion = null, Action configure = null) { var builder = new DbContextOptionsBuilder(); var npgsqlBuilder = new NpgsqlDbContextOptionsBuilder(builder); @@ -461,6 +545,7 @@ private NpgsqlTypeMappingSource CreateTypeMappingSource(Version postgresVersion npgsqlBuilder.MapRange("floatrange"); npgsqlBuilder.MapRange("dummyrange", subtypeName: "dummy"); npgsqlBuilder.SetPostgresVersion(postgresVersion); + configure?.Invoke(npgsqlBuilder); var options = new NpgsqlSingletonOptions(); options.Initialize(builder.Options); @@ -517,6 +602,16 @@ private class DummyType; private class UnknownType; + private enum DummyEnum + { + Value + } + + private enum OtherDummyEnum + { + Value + } + private class ByteArrayEntity { public int Id { get; set; }