Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions src/EFCore.PG/Infrastructure/Internal/EnumDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PgNameAttribute>()?.PgName ?? nameTranslator.TranslateMemberName(x.Name));

var labels = new Dictionary<object, string>();
// 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<object, string?>();
foreach (var field in clrType.GetFields(BindingFlags.Static | BindingFlags.Public))
{
var value = field.GetValue(null)!;
var pgName = field.GetCustomAttribute<PgNameAttribute>()?.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;
}

/// <inheritdoc />
Expand Down
96 changes: 96 additions & 0 deletions test/EFCore.PG.Tests/Infrastructure/EnumDefinitionTest.cs
Original file line number Diff line number Diff line change
@@ -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<InvalidOperationException>(
() => 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<InvalidOperationException>(
() => 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,
}
}