Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Defining aliases for enum values #20202

Open
Tracked by #240
xamadev opened this issue Feb 27, 2020 · 10 comments
Open
Tracked by #240

Defining aliases for enum values #20202

xamadev opened this issue Feb 27, 2020 · 10 comments

Comments

@xamadev
Copy link

xamadev commented Feb 27, 2020

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.

  • ID: 2a36c111-5524-f245-4012-227a30310d3f
  • Version Independent ID: 84e908b5-f5ec-1d31-4123-dd148e73fc43
  • Content: EnumToStringConverter<TEnum> Class (Microsoft.EntityFrameworkCore.Storage.ValueConversion)
  • Content Source: [dotnet/xml/Microsoft.EntityFrameworkCore.Storage.ValueConversion/EnumToStringConverter1.xml](https://github.com/aspnet/EntityFramework.ApiDocs/blob/live/dotnet/xml/Microsoft.EntityFrameworkCore.Storage.ValueConversion/EnumToStringConverter1.xml)
  • Product: entity-framework-core
  • GitHub Login: @dotnet-bot
  • Microsoft Alias: divega
@ajcvickers
Copy link
Member

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

@xamadev
Copy link
Author

xamadev commented Feb 28, 2020

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
Copy link
Member

@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
Copy link
Author

xamadev commented Mar 3, 2020

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

@CZEMacLeod
Copy link

@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
Copy link
Member

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

@ajcvickers ajcvickers reopened this Mar 6, 2020
@ajcvickers ajcvickers transferred this issue from dotnet/EntityFramework.Docs Mar 6, 2020
@CZEMacLeod
Copy link

@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
Copy link

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
Copy link

haacked commented Feb 1, 2023

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
Copy link

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants