diff --git a/src/EFCore.PG/Infrastructure/Internal/EnumDefinition.cs b/src/EFCore.PG/Infrastructure/Internal/EnumDefinition.cs index b02ad9b781..8802ad748d 100644 --- a/src/EFCore.PG/Infrastructure/Internal/EnumDefinition.cs +++ b/src/EFCore.PG/Infrastructure/Internal/EnumDefinition.cs @@ -85,10 +85,36 @@ public EnumDefinition( ClrType = clrType; NameTranslator = nameTranslator; - Labels = clrType.GetFields(BindingFlags.Static | BindingFlags.Public) - .ToDictionary( - x => x.GetValue(null)!, - x => x.GetCustomAttribute()?.PgName ?? nameTranslator.TranslateMemberName(x.Name)); + + var labels = new Dictionary(); + // Tracks the [PgName] attribute value for each enum value (null if no [PgName] was specified). + // Used to detect conflicting [PgName] mappings for different labels that share the same underlying value. + var pgNames = new Dictionary(); + foreach (var field in clrType.GetFields(BindingFlags.Static | BindingFlags.Public)) + { + var value = field.GetValue(null)!; + var pgName = field.GetCustomAttribute()?.PgName; + + if (labels.TryGetValue(value, out _)) + { + var existingPgName = pgNames[value]; + + // If either the existing or current field has a [PgName] attribute, they must match. + if ((pgName is not null || existingPgName is not null) + && pgName != existingPgName) + { + throw new InvalidOperationException( + $"Enum '{clrType.Name}' has multiple members with the same value '{value}' but with different [PgName] mappings ('{existingPgName ?? "(none)"}' and '{pgName ?? "(none)"}'). All members that share the same value must have identical [PgName] mappings."); + } + + continue; + } + + labels.Add(value, pgName ?? nameTranslator.TranslateMemberName(field.Name)); + pgNames.Add(value, pgName); + } + + Labels = labels; } /// diff --git a/test/EFCore.PG.Tests/Infrastructure/EnumDefinitionTest.cs b/test/EFCore.PG.Tests/Infrastructure/EnumDefinitionTest.cs new file mode 100644 index 0000000000..909d3820fe --- /dev/null +++ b/test/EFCore.PG.Tests/Infrastructure/EnumDefinitionTest.cs @@ -0,0 +1,96 @@ +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +using Npgsql.NameTranslation; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; + +public class EnumDefinitionTest +{ + [ConditionalFact] + public void Enum_with_duplicate_values_uses_first_label() + { + var definition = new EnumDefinition( + typeof(EnumWithDuplicateValues), + name: null, + schema: null, + new NpgsqlSnakeCaseNameTranslator()); + + Assert.Equal(2, definition.Labels.Count); + Assert.Equal("alive", definition.Labels[EnumWithDuplicateValues.Alive]); + Assert.Equal("deceased", definition.Labels[EnumWithDuplicateValues.Deceased]); + } + + [ConditionalFact] + public void Enum_with_duplicate_values_and_same_pg_name_succeeds() + { + var definition = new EnumDefinition( + typeof(EnumWithDuplicateValuesAndSamePgName), + name: null, + schema: null, + new NpgsqlSnakeCaseNameTranslator()); + + Assert.Equal(2, definition.Labels.Count); + Assert.Equal("custom_alive", definition.Labels[EnumWithDuplicateValuesAndSamePgName.Alive]); + Assert.Equal("deceased", definition.Labels[EnumWithDuplicateValuesAndSamePgName.Deceased]); + } + + [ConditionalFact] + public void Enum_with_duplicate_values_and_different_pg_names_throws() + { + var exception = Assert.Throws( + () => new EnumDefinition( + typeof(EnumWithDuplicateValuesAndDifferentPgNames), + name: null, + schema: null, + new NpgsqlSnakeCaseNameTranslator())); + + Assert.Contains("different", exception.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("[PgName]", exception.Message); + } + + [ConditionalFact] + public void Enum_with_duplicate_values_where_only_one_has_pg_name_throws() + { + var exception = Assert.Throws( + () => new EnumDefinition( + typeof(EnumWithDuplicateValuesOnePgName), + name: null, + schema: null, + new NpgsqlSnakeCaseNameTranslator())); + + Assert.Contains("different", exception.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("[PgName]", exception.Message); + } + + private enum EnumWithDuplicateValues + { + Alive, + Deceased, + Comatose = Alive, + } + + private enum EnumWithDuplicateValuesAndSamePgName + { + [PgName("custom_alive")] + Alive, + Deceased, + [PgName("custom_alive")] + Comatose = Alive, + } + + private enum EnumWithDuplicateValuesAndDifferentPgNames + { + [PgName("label_a")] + Alive, + Deceased, + [PgName("label_b")] + Comatose = Alive, + } + + private enum EnumWithDuplicateValuesOnePgName + { + [PgName("custom_alive")] + Alive, + Deceased, + Comatose = Alive, + } +}