dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.68k stars 3.16k forks source link

Defining aliases for enum values #20202

Open xamadev opened 4 years ago

xamadev commented 4 years ago

Hi,

is it possible to define aliases for enum's values like using the Description annotation?


Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

ajcvickers commented 4 years ago

@xamadev Can you explain a bit more what you mean by "aliases" in this case?

xamadev commented 4 years ago

Sure. Imagine you have a status column in database which contains string values of "S" or "E". In code I'd like to map them to an enum with values Enum.Success and Enum.Error which I call aliases in my question. Therefore I need a mapping between the String value and the corresponding enum value.

ajcvickers commented 4 years ago

@xamadev The way to handle that is with a custom value converter. Something like:

public enum Worm
{
    Slow,
    Earth
}

public class WormConverter : ValueConverter<Worm, string>
{
    public WormConverter()
        : base(v => Convert(v), v => Convert(v))
    {
    }

    private static string Convert(Worm value)
    {
        return value switch
        {
            Worm.Slow => "S",
            Worm.Earth => "E",
            _ => throw new ArgumentOutOfRangeException(nameof(value), value, null)
        };
    }

    private static Worm Convert(string value)
    {
        return value switch
        {
            "S" => Worm.Slow,
            "E" => Worm.Earth,
            _ => throw new ArgumentOutOfRangeException(nameof(value), value, null)
        };
    }
}
xamadev commented 4 years ago

All right, thanks for your help. Maybe it's worth introducing a data annotation to define such aliases / mappings? What do you think?

CZEMacLeod commented 4 years ago

@xamadev I'm not sure if it should be a new attribute - but this feels like it should line up with the enum as string serialization attributes like System.Runtime.Serialization.EnumMember

ajcvickers commented 4 years ago

@CZEMacLeod EnumMember is interesting--I wasn't aware of it before now. We will discuss.

CZEMacLeod commented 4 years ago

@ajcvickers System.Runtime.Serialization.EnumMember is used by DataContractSerializer and is also used by Json.NET's Newtonsoft.Json.Converters.StringEnumConverter.

The class Newtonsoft.Json.Utilities.EnumUtils seems to have some good code for Enum->String and String->Enum handling.

I'm not sure about Microsoft's System.Text.Json implementation JsonStringEnumConverter but it doesn't look like it :( I got as far as System.Text.Json.Serialization.Converters.EnumConverter but there is a comment in the docs stating that

Attributes from the [System.Runtime.Serialization](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.serialization) namespace aren't currently supported in System.Text.Json.

Although it does look like there are extension points for it with custom converters.

It does seem like there is a fair bit of re-inventing the wheel going on here between all these projects doing value conversion and specifically Enum/String conversion...

haraldkofler commented 4 years ago

From my point of view, adding the possibility to automatically export the enum text and value mapping into the field description would be a great feature to increase the readability of such columns... db-tools like dbeaver show the column description if you hover the column.

haacked commented 1 year ago

I wrote a value converter that can handle EnumMemberAttribute:

/// <summary>
/// A value converter for enums that use the <see cref="EnumMemberAttribute"/> value as the stored value.
/// </summary>
/// <remarks>
/// It turns out that EF Core doesn't respect the EnumMemberAttribute yet.
/// </remarks>
/// <typeparam name="TEnum">The enum type.</typeparam>
public class EnumMemberValueConverter<TEnum> : ValueConverter<TEnum, string> where TEnum : struct, Enum
{
    // Yeah, these dictionaries are never collected, but they're going to generally be small and
    // there's not going to be too many of them.
    static readonly Dictionary<string, TEnum> StringToEnumLookup = CreateStringToEnumLookup();
    static readonly Dictionary<TEnum, string> EnumToStringLookup = CreateEnumToStringLookup(StringToEnumLookup);

    public EnumMemberValueConverter()
        : base(
            value => GetStringFromEnum(value),
            storedValue => GetEnumFromString(storedValue))
    {
    }

    static TEnum GetEnumFromString(string value) => StringToEnumLookup.TryGetValue(value, out var enumValue)
        ? enumValue
        : default;

    static string GetStringFromEnum(TEnum value) => EnumToStringLookup.TryGetValue(value, out var enumValue)
        ? enumValue
        : string.Empty;

    static Dictionary<string, TEnum> CreateStringToEnumLookup() =>
        Enum.GetValues<TEnum>().ToDictionary(value => value.GetEnumMemberValueName(), value => value);
    static Dictionary<TEnum, string> CreateEnumToStringLookup(Dictionary<string, TEnum> stringToEnumLookup) =>
        stringToEnumLookup.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
}

public static class EnumExtensions {
    public static string GetEnumMemberValueName(this Enum? item)
        => item.GetEnumAttributeValue<EnumMemberAttribute>(attr => attr.Value);

    static string GetEnumAttributeValue<TAttribute>(this Enum? item, Func<TAttribute, string?> getAttributeValue)
        where TAttribute : Attribute
    {
        var field = item?.GetType().GetField(item.ToString());
        if (field is null)
            return string.Empty;

        var memberAttribute = field.GetCustomAttribute<TAttribute>(inherit: false);
        if (memberAttribute is not null)
        {
            return getAttributeValue(memberAttribute) ?? field.Name;
        }

        return field.Name;
    }
}

Note that these dictionaries are never collected until the process goes down, but the expectation is there won't be too many values so the overhead should be small.

If an enum value doesn't have the attribute, then the normal conversion to/from string applies.

Just register the converter in your OnModelCreating method of your DbContext derived class like so:

modelBuilder.Entity<MyEntity>()
            .Property(a => a.EnumProperty)
            .HasConversion(new EnumMemberValueConverter<EnumPropertyType>());

That will let you use enum aliases until EF supports it natively.

CZEMacLeod commented 1 year ago

@haacked This looks like a good start, although I think it would be better to use a source generator to build the converter with the finite list of values rather than reflection to build the dictionaries. Also it does not cover enums which are defined as Flags (which most of my enums stored as text in the database happen to be). Although this may be a subset of subset of the use case and not required by many, it would be better if any given solution actually covered this case.