Skip to content

Commit 7b0887f

Browse files
authored
Fix mapping of enums with duplicate labels (#3743)
Fixes #3742
1 parent f7657cd commit 7b0887f

File tree

2 files changed

+126
-4
lines changed

2 files changed

+126
-4
lines changed

src/EFCore.PG/Infrastructure/Internal/EnumDefinition.cs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,36 @@ public EnumDefinition(
8585
ClrType = clrType;
8686

8787
NameTranslator = nameTranslator;
88-
Labels = clrType.GetFields(BindingFlags.Static | BindingFlags.Public)
89-
.ToDictionary(
90-
x => x.GetValue(null)!,
91-
x => x.GetCustomAttribute<PgNameAttribute>()?.PgName ?? nameTranslator.TranslateMemberName(x.Name));
88+
89+
var labels = new Dictionary<object, string>();
90+
// Tracks the [PgName] attribute value for each enum value (null if no [PgName] was specified).
91+
// Used to detect conflicting [PgName] mappings for different labels that share the same underlying value.
92+
var pgNames = new Dictionary<object, string?>();
93+
foreach (var field in clrType.GetFields(BindingFlags.Static | BindingFlags.Public))
94+
{
95+
var value = field.GetValue(null)!;
96+
var pgName = field.GetCustomAttribute<PgNameAttribute>()?.PgName;
97+
98+
if (labels.TryGetValue(value, out _))
99+
{
100+
var existingPgName = pgNames[value];
101+
102+
// If either the existing or current field has a [PgName] attribute, they must match.
103+
if ((pgName is not null || existingPgName is not null)
104+
&& pgName != existingPgName)
105+
{
106+
throw new InvalidOperationException(
107+
$"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.");
108+
}
109+
110+
continue;
111+
}
112+
113+
labels.Add(value, pgName ?? nameTranslator.TranslateMemberName(field.Name));
114+
pgNames.Add(value, pgName);
115+
}
116+
117+
Labels = labels;
92118
}
93119

94120
/// <inheritdoc />
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
2+
using Npgsql.NameTranslation;
3+
4+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
5+
6+
public class EnumDefinitionTest
7+
{
8+
[ConditionalFact]
9+
public void Enum_with_duplicate_values_uses_first_label()
10+
{
11+
var definition = new EnumDefinition(
12+
typeof(EnumWithDuplicateValues),
13+
name: null,
14+
schema: null,
15+
new NpgsqlSnakeCaseNameTranslator());
16+
17+
Assert.Equal(2, definition.Labels.Count);
18+
Assert.Equal("alive", definition.Labels[EnumWithDuplicateValues.Alive]);
19+
Assert.Equal("deceased", definition.Labels[EnumWithDuplicateValues.Deceased]);
20+
}
21+
22+
[ConditionalFact]
23+
public void Enum_with_duplicate_values_and_same_pg_name_succeeds()
24+
{
25+
var definition = new EnumDefinition(
26+
typeof(EnumWithDuplicateValuesAndSamePgName),
27+
name: null,
28+
schema: null,
29+
new NpgsqlSnakeCaseNameTranslator());
30+
31+
Assert.Equal(2, definition.Labels.Count);
32+
Assert.Equal("custom_alive", definition.Labels[EnumWithDuplicateValuesAndSamePgName.Alive]);
33+
Assert.Equal("deceased", definition.Labels[EnumWithDuplicateValuesAndSamePgName.Deceased]);
34+
}
35+
36+
[ConditionalFact]
37+
public void Enum_with_duplicate_values_and_different_pg_names_throws()
38+
{
39+
var exception = Assert.Throws<InvalidOperationException>(
40+
() => new EnumDefinition(
41+
typeof(EnumWithDuplicateValuesAndDifferentPgNames),
42+
name: null,
43+
schema: null,
44+
new NpgsqlSnakeCaseNameTranslator()));
45+
46+
Assert.Contains("different", exception.Message, StringComparison.OrdinalIgnoreCase);
47+
Assert.Contains("[PgName]", exception.Message);
48+
}
49+
50+
[ConditionalFact]
51+
public void Enum_with_duplicate_values_where_only_one_has_pg_name_throws()
52+
{
53+
var exception = Assert.Throws<InvalidOperationException>(
54+
() => new EnumDefinition(
55+
typeof(EnumWithDuplicateValuesOnePgName),
56+
name: null,
57+
schema: null,
58+
new NpgsqlSnakeCaseNameTranslator()));
59+
60+
Assert.Contains("different", exception.Message, StringComparison.OrdinalIgnoreCase);
61+
Assert.Contains("[PgName]", exception.Message);
62+
}
63+
64+
private enum EnumWithDuplicateValues
65+
{
66+
Alive,
67+
Deceased,
68+
Comatose = Alive,
69+
}
70+
71+
private enum EnumWithDuplicateValuesAndSamePgName
72+
{
73+
[PgName("custom_alive")]
74+
Alive,
75+
Deceased,
76+
[PgName("custom_alive")]
77+
Comatose = Alive,
78+
}
79+
80+
private enum EnumWithDuplicateValuesAndDifferentPgNames
81+
{
82+
[PgName("label_a")]
83+
Alive,
84+
Deceased,
85+
[PgName("label_b")]
86+
Comatose = Alive,
87+
}
88+
89+
private enum EnumWithDuplicateValuesOnePgName
90+
{
91+
[PgName("custom_alive")]
92+
Alive,
93+
Deceased,
94+
Comatose = Alive,
95+
}
96+
}

0 commit comments

Comments
 (0)