Why is the order of the enums important when specifying circular enum values in .NET 5?

Solution for Why is the order of the enums important when specifying circular enum values in .NET 5?
is Given Below:

I’m using enum circular reference to boil down some existing enums to a fewer values.
It works well for my issue, since its a for a transitions period, where old enum values do occur in history, but no new entries with the obsolete values will be created.

I’m not looking for an alternative approach, but I stumbled upon this strange issue where the order of the enums, will affect the the serilized value in an unexpected manner.

I have this enum:

    public enum CivilStatusEnum
    {
        None = 0,
        Married = 1,
        Cohabiting = Married,
        Alone = 3,
        DivorcedOrSeparated = Alone,
        WidowOrWidower = Alone,
    }

I assigned “DivorcedOrSeparated” = Alone (3). Now when I convert the enum to string I get the value “DivorcedOrSeparated”.

Console.PrintLine(CivilStatusEnum.Alone.ToString());

Output:

DivorcedOrSeparated

I have an example code, with tests and their expected outcome. As you can see the test are failing. But If I change the order of the enums, the tests will parse.

enter image description here

    [Theory]
    [InlineData(CivilStatusEnum.Alone, "Alone")]
    [InlineData(CivilStatusEnum.DivorcedOrSeparated, "Alone")]
    [InlineData(CivilStatusEnum.WidowOrWidower, "Alone")]
    [InlineData(CivilStatusEnum.None, "None")]
    [InlineData(CivilStatusEnum.Married, "Married")]
    [InlineData(CivilStatusEnum.Cohabiting, "Married")]
    public void Test(CivilStatusEnum input, string expected)
    {
        var text = input.ToString();
        text.Should().Be(expected);
    }

I can’t seem to find a rationel explanation for why the order matters for tostring and serilization.

Is this a bug in .NET 5, or am I missing something?
If this behaviour is deliberate, how does it determind which enum name will be the output of tostring?

Thank you 🙂

Enum.ToString performs a binary search.

Indeed, ToString calls InternalFormat, which calls GetEnumName. That method performs a binary search in the array returned by EnumInfo.Values.

I assume that array is filled in increasing order of the underlying values (otherwise a binary search would not work), and in the order in which the values were declared in the source code if they are equal. That makes the result of the search dependent on the order of declaration.

To illustrate the effects of this binary search, consider these two enum definitions:

enum Test1 { A = 0, B = 0, C = 0 }
enum Test2 { A = 0, B = 0, C = 0, D = 0, E = 0 }

What would be the result of Test1.A.ToString()? Note that the value of Test1.A is 0. The binary search will first consider the element in the middle of the list, which is B with the value 0. That value equals the value we are searching for, so Test1.A.ToString() returns "B". If the value found was higher than the value being searched for, the search would continue in the lower half of the list. If the value found was lower than the value being searched for, the search would continue in the higher half of the list.

The same is true for all the constants in the enum, given they all have the same value. So, Test1.C.ToString() will equally return "B".

Similarly, Test2.A.ToString() returns "C", as expected.

Note however that while this behavior seems to be predictable in the current version of .NET, it is undefined, and could change in a future version.

This is not a bug in .NET 5. After all, it is impossible for both of the following to be true:

CivilStatusEnum.Alone.ToString() ==  "Alone"
CivilStatusEnum.DivorcedOrSeparated.ToString() == "DivorcedOrSeparated"

The reason is of course that CivilStatusEnum.Alone == CivilStatusEnum.DivorcedOrSeparated.

Here’s what the documentation has to say about this:

If multiple enumeration members have the same underlying value and you attempt to retrieve the string representation of an enumeration member’s name based on its underlying value, your code should not make any assumptions about which name the method will return.

I know you said you’re not looking for an alternative approach, but another way to do this and avoid this kind of problem is to use flags:

[Flags]
public enum CivilStatusEnum
{
    None = 0,
    Married = 1,
    Cohabiting = 3, //Married | 2
    Alone = 4,
    DivorcedOrSeparated = 12, //Alone | 8
    WidowOrWidower = 20, //Alone | 16
}

And then ToString will produce the right answer and you can check if some one is married or alone with the following code:

bool IsMarried(CivilStatusEnum e){
    return ((int)e&1) == 1;
}

bool IsAlone(CivilStatusEnum e){
    return ((int)e&4) == 4;
}