As you know, converting an int to an enum is straightforward. Doing it generically with type/range checking isn't. I looked for a reusable example since this seems like a common situation but did not find a suitable one. If it exists, please let me know. Below is a C# extension method that encapsulates this behavior. Any feedback is welcome.

public static T ToEnum<T>(this int value)
{
    Type type = typeof(T);

    if (!type.IsEnum)
    {
        throw new ArgumentException($"{type} is not an enum.");
    }

    if (!type.IsEnumDefined(value))
    {
        throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
    }

    return (T)Enum.ToObject(type, value);
}
  • 3
    Be careful withIsEnumDefined. It won't work if using Flagsand the value represents a combination. – Jason Tyler Aug 16 at 13:24
  • @JasonTyler Correct. It may not work as expected in some cases. It does return the enum containing combined flags. Not sure what the use case is for getting the combined flags and converting them from their ordinals back to flags. – user1424660 Aug 16 at 14:13
  • 1
    where T : struct could use a generic constraint. – JᴀʏMᴇᴇ Aug 16 at 14:27
up vote 9 down vote accepted

As Jason Tyler said in a comment, this has a problem that [Flags] enumerations won't be properly handled.

I ran this in C# interactive, and it threw an exception (as expected, but not desired) for a valid [Flags] value:

> public static T ToEnum<T>(this int value)
. {
.     Type type = typeof(T);
. 
.     if (!type.IsEnum)
.     {
.         throw new ArgumentException($"{type} is not an enum.");
. 
.     }
. 
.     if (!type.IsEnumDefined(value))
.     {
.         throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
.     }
. 
.     return (T)Enum.ToObject(type, value);
. }
> [Flags] public enum Test { Flag1 = 0x01, Flag2 = 0x02 }
> ToEnum<Test>(3);
3 is not a valid ordinal of type Submission#1+Test.
  + Submission#0.ToEnum<T>(int)

So, we might have to look at a different way of going about this.

There's an answer on Stack Overflow, but I don't like it. They use string comparison and validation, and it just feels like a hack.

So what can we do?

Well, we have Enum.GetValues, I bet we can come up with a solution there, assuming we interpret all the values as a numeric type.

The hard part is picking the type. Enums can be any numeric type, so we have to decide on one.

Fun fact: every numeric type can be cast to the others in C#, there just might be overflow issues. So if we look at all the numeric types, we can find that we should always be able to cast them to a long or ulong, depending on whether they're signed or not.

OK, so how do we figure out what number type it is?

There's another magic function in Enum: Enum.GetUnderlyingType, I think that'll do.

Of course, this also means we can no longer take an int input, because we want to support any number. We could take in an object, but then we have to deal with boxing/unboxing, and that can get prohibitively expensive with frequent use, but we'll work that out-of-the-way later.

We can use some bit-fiddling to find out if a value is only composed of valid Enum values. Basically, we'll do the following:

  • Loop through the valid flags options:
    • If providedValue ∧ enumValue > 0, then providedValue = providedValue ⊕ enumValue
  • If providedValue > 0 then it is not composed strictly of the valid enum options

For those unfamiliar with boolean algebra:

  • = "AND", take all bits of both values and compare them such that the result value only contains those that are 1 in each value, this is & in C#;
  • = "XOR", take all bits of both values and compare them such that the result value only contains those that are 1 in one value, and 0 in another, this is ^ in C#;

So, if we rewrite this, it gets a bit ugly, but we'll clean some of that up:

public static TEnum ToEnum<TEnum>(this object value)
{
    Type type = typeof(TEnum);

    if (!type.IsEnum)
    {
        throw new ArgumentException($"{type} is not an enum.");
    }

    if (type.GetCustomAttributes(typeof(FlagsAttribute), true).Length > 0)
    {
        var values = Enum.GetValues(type);
        switch (Enum.GetUnderlyingType(type).FullName)
        {
            case "System.Byte":
                {
                    var myVal = (byte)value;
                    foreach (byte val in values)
                    {
                        if ((myVal & val) > (byte)0)
                        {
                            myVal ^= val;
                        }
                    }
                    if (myVal > (byte)0)
                    {
                        throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
                    }
                }
                break;
            case "System.SByte":
                {
                    var myVal = (sbyte)value;
                    foreach (sbyte val in values)
                    {
                        if ((myVal & val) > (sbyte)0)
                        {
                            myVal ^= val;
                        }
                    }
                    if (myVal > (sbyte)0)
                    {
                        throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
                    }
                }
                break;
            case "System.UInt16":
                {
                    var myVal = (ushort)value;
                    foreach (ushort val in values)
                    {
                        if ((myVal & val) > (ushort)0)
                        {
                            myVal ^= val;
                        }
                    }
                    if (myVal > (ushort)0)
                    {
                        throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
                    }
                }
                break;
            case "System.Int16":
                {
                    var myVal = (short)value;
                    foreach (short val in values)
                    {
                        if ((myVal & val) > (short)0)
                        {
                            myVal ^= val;
                        }
                    }
                    if (myVal > (short)0)
                    {
                        throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
                    }
                }
                break;
            case "System.UInt32":
                {
                    var myVal = (uint)value;
                    foreach (uint val in values)
                    {
                        if ((myVal & val) > (uint)0)
                        {
                            myVal ^= val;
                        }
                    }
                    if (myVal > (uint)0)
                    {
                        throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
                    }
                }
                break;
            case "System.Int32":
                {
                    var myVal = (int)value;
                    foreach (int val in values)
                    {
                        if ((myVal & val) > (int)0)
                        {
                            myVal ^= val;
                        }
                    }
                    if (myVal > (int)0)
                    {
                        throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
                    }
                }
                break;
            case "System.UInt64":
                {
                    var myVal = (ulong)value;
                    foreach (ulong val in values)
                    {
                        if ((myVal & val) > (ulong)0)
                        {
                            myVal ^= val;
                        }
                    }
                    if (myVal > (ulong)0)
                    {
                        throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
                    }
                }
                break;
            case "System.Int64":
                {
                    var myVal = (long)value;
                    foreach (long val in values)
                    {
                        if ((myVal & val) > (long)0)
                        {
                            myVal ^= val;
                        }
                    }
                    if (myVal > (long)0)
                    {
                        throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
                    }
                }
                break;
            default:
                throw new ArgumentException($"{type} does not have a valid backing type ({underlyingType}).");
                break;
        }
    }
    else
    {
        if (!type.IsEnumDefined(value))
        {
            throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
        }
    }

    return (TEnum)Enum.ToObject(type, value);
}

This will work as expected on [Flags] enumerations, which is great! But, there's some improvement to be made.

First and foremost: how do we get rid of that object parameter?

Well, we have a few options, but the easiest is to redefine our ToEnum on each numeric type, and create a version that takes a dynamic. It's ugly, but works:

public static TEnum ToEnum<TEnum>(this byte value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this sbyte value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this ushort value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this short value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this uint value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this int value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this ulong value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this long value) => DynamicToEnum<TEnum>(value);

private static TEnum DynamicToEnum<TEnum>(dynamic value)
{
    ...

For each repeated case body, there's no good way to do this. With the use dynamic, we lose all safety of our types. There's no way to constrain it generically, because we can't say "it can be any number."

The best we could do would be a local function something like the following:

void testEnum<T>(T myVal, Func<T, bool> aboveZero, Func<T, T, T> and, Func<T, T, T> xor)
{
    foreach (T val in values)
    {
        if (aboveZero(and(myVal, val)))
        {
            myVal = xor(myVal, val);
        }
    }
    if (aboveZero(myVal))
    {
        throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
    }
}

Which means, we can use it to create a more readable version of our function:

public static TEnum ToEnum<TEnum>(this byte value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this sbyte value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this ushort value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this short value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this uint value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this int value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this ulong value) => DynamicToEnum<TEnum>(value);
public static TEnum ToEnum<TEnum>(this long value) => DynamicToEnum<TEnum>(value);

private static TEnum DynamicToEnum<TEnum>(dynamic value)
{
    Type type = typeof(TEnum);

    if (!type.IsEnum)
    {
        throw new ArgumentException($"{type} is not an enum.");
    }

    if (type.GetCustomAttributes(typeof(FlagsAttribute), true).Length > 0)
    {
        var values = Enum.GetValues(type);

        void testEnum<T>(T myVal, Func<T, bool> aboveZero, Func<T, T, T> and, Func<T, T, T> xor)
        {
            foreach (T val in values)
            {
                if (aboveZero(and(myVal, val)))
                {
                    myVal = xor(myVal, val);
                }
            }
            if (aboveZero(myVal))
            {
                throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
            }
        }

        var underlyingType = Enum.GetUnderlyingType(type).FullName;
        switch (underlyingType)
        {
            case "System.Byte":
                testEnum((Byte)value, (v) => v > (Byte)0, (v1, v2) => (Byte)(v1 & v2), (v1, v2) => (Byte)(v1 ^ v2));
                break;
            case "System.SByte":
                testEnum((SByte)value, (v) => v > (SByte)0, (v1, v2) => (SByte)(v1 & v2), (v1, v2) => (SByte)(v1 ^ v2));
                break;
            case "System.UInt16":
                testEnum((UInt16)value, (v) => v > (UInt16)0, (v1, v2) => (UInt16)(v1 & v2), (v1, v2) => (UInt16)(v1 ^ v2));
                break;
            case "System.Int16":
                testEnum((Int16)value, (v) => v > (Int16)0, (v1, v2) => (Int16)(v1 & v2), (v1, v2) => (Int16)(v1 ^ v2));
                break;
            case "System.UInt32":
                testEnum((UInt32)value, (v) => v > (UInt32)0, (v1, v2) => (UInt32)(v1 & v2), (v1, v2) => (UInt32)(v1 ^ v2));
                break;
            case "System.Int32":
                testEnum((Int32)value, (v) => v > (Int32)0, (v1, v2) => (Int32)(v1 & v2), (v1, v2) => (Int32)(v1 ^ v2));
                break;
            case "System.UInt64":
                testEnum((UInt64)value, (v) => v > (UInt64)0, (v1, v2) => (UInt64)(v1 & v2), (v1, v2) => (UInt64)(v1 ^ v2));
                break;
            case "System.Int64":
                testEnum((Int64)value, (v) => v > (Int64)0, (v1, v2) => (Int64)(v1 & v2), (v1, v2) => (Int64)(v1 ^ v2));
                break;
            default:
                throw new ArgumentException($"{type} does not have a valid backing type ({underlyingType}).");
                break;
        }
    }
    else
    {
        if (!type.IsEnumDefined(value))
        {
            throw new ArgumentException($"{value} is not a valid ordinal of type {type}.");
        }
    }

    return (TEnum)Enum.ToObject(type, value);
}

Overboard? Probably. But does it work? Oh yeah.

Additionally, as Peter Taylor mentions, you can use where TEnum : struct to help reduce some of the run-time issues, and bring them to the compile-type.

  • This needs to be compared with the built-in API - I don't trust it ;-) I bet you're already doing this... – t3chb0t Aug 16 at 14:58
  • @t3chb0t I've tested it on everything, assuming one wants to guarantee that the value is only a list of the appropriate flags, then it works just fine. – 202_accepted Aug 16 at 14:59
  • I mean performance-wise... there are a lot of loops involeved so I'm very sceptical about them being faster then unboxing. – t3chb0t Aug 16 at 15:01
  • The only performance item I mention is the use of dynamic vs. object, feel free to verify but I'm 99% sure dynamic is faster. (I've used it in production for that exact reason.) – 202_accepted Aug 16 at 15:03
  • @202_accepted: I know this is maybe theoretical, but if a flagged enum has negative values the xor-mechanism doesn't work properly. It is unwise to use flags with negative values, but it is possible... – Henrik Hansen Aug 17 at 8:44
public static T ToEnum<T>(this int value)
{
    Type type = typeof(T);

    if (!type.IsEnum)
    {
        throw new ArgumentException($"{type} is not an enum.");
    }

You haven't said anything specific about which version of C# you're using, so I'm going to address two cases.

If you're able to be at the cutting edge and use C# v7.3 then you can simplify this to

public static T ToEnum<T>(this int value)
    where T : Enum
{
    Type type = typeof(T);

If for whatever reason you're forced to use an earlier version of C# you can still at least detect some problems at compile-time with

public static T ToEnum<T>(this int value)
    where T : struct
{
    Type type = typeof(T);

    if (!type.IsEnum)
    {
        throw new ArgumentException($"{type} is not an enum.");
    }

Note that it is a matter of preference or local style whether in 7.3 you combine both tests: the only subtype of Enum which isn't a struct is Enum itself, so the gain is minor.


Depending on your use case, this might not be a problem, but enums aren't necessarily ints. In particular, the following is legal:

enum Foo : long
{
    Bar = 0x1234567890
}

So you might need to use a wider type for value.

  • A small sidenote: don't trust ReSharper with that where T : Enum, it doesn't understand it until today's update :-o – t3chb0t Aug 16 at 14:47
  • The code uses .NET Core 2.1. The where T : Enum/struct syntax is cleaner if I could use #if defined to compile based on language version. Interesting idea. Researching it right now. – user1424660 Aug 16 at 14:52
  • @user1424660, .Net Core 2.1 is largely orthogonal to the version of the language. I'm using where T : Enum in a Net Standard 1.3 library. It just requires editing the csproj to add a property <LangVersion>7.3</LangVersion>, and having a recent update of VS. – Peter Taylor Aug 16 at 15:03

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.