Click here to Skip to main content
15,879,535 members
Articles / Programming Languages / C#
Article

'Extending' the Enum Class to Validate the Enum Values with the Flags Attribute

Rate me:
Please Sign up or sign in to vote.
4.58/5 (5 votes)
4 Jan 2008CPOL4 min read 62.1K   297   41   6
Use extension methods to augment the enums to validate the values and more
Screenshot -

Introduction

Every time you have a set of ordinal values that are related, it is a good practice to put them in an enumeration rather than leave them as constant values.

Enumerations have been around at least since C++ and .NET continues to do so and has even added new features like flags attribute for the type or description for the elements.

Background

Let's consider a simple C# enumeration:

C#
enum myenum : byte
{
    a = 1,
    b = 2,
    c = 3
}

You can easily assign a value that is the type of the underlying type like:

C#
myenum eval = default(myenum);//that is actually 0
eval = (myenum)4;

The problem lies in the fact that 0 and 4 are not part of myenum, and the compiler or the runtime won't complain about it and that can cause trouble when they are used in your code. It appears that the only check is against the underlying type, not the enum.
In order to check for the values, you can use the static Enum.IsDefined method that would validate the enum value. But things get more complicated when dealing with enums with flags like below:

C#
[Flags]// Define an Enum with FlagsAttribute.
enum MultiHue : sbyte
{
    [Description("no Color")]Black = 0,
    [Description("pure red")]Red = 1,
    [Description("pure green")]Green = 2,
    //[Description("pure yellow")]Yellow = 3,
    //Green | Red number 3 is deliberately omitted
    [Description("pure blue")]Blue = 4,
    [Description("composite green + blue")]Cyan = Green | Blue,//6
    [Description("composite red + blue")]Magenta = Red | Blue,//5
    [Description("composite red + blue + green")]White = Red | Blue | Green,//7
    [Description("not a valid color")]BadColor = 127
};

If you run Enum.IsDefined(typeof(MultiHue),3), the return value is false, but while this is true, for flags you might expect a different outcome since 3 is 1 | 2, so it should be a valid value. Even the ToString() instance method would return 'Red, Green', not the number 3 as an undefined value.

Validate the Value of the Enumeration

Starting with C# 3.0, in its quest to improve usability, Microsoft introduced the extension methods that we can also use as an elegant way to validate the enums value.
For an enumeration with no flags, the validation is built in the framework, but using this language feature makes it more readable:

C#
public static bool IsDefined(this System.Enum value)
{
    return System.Enum.IsDefined(value.GetType(), value);
}

Using this extension method, validation becomes very elegant:

C#
MultiHue mh = MultiHue.Blue;
bool isdefined = mh.IsDefined();

Validating an enum with flags is a little more complicated since the IsDefined method is not enough to make the difference between Defined and Valid:

C#
public static bool IsValidEnumValue(this System.Enum value)
{
    if (value.HasFlags())
        return IsFlagsEnumDefined(value);
    else
        return value.IsDefined();
}

private static bool IsFlagsEnumDefined(System.Enum value)
{// modeled after Enum's InternalFlagsFormat
    Type underlyingenumtype = Enum.GetUnderlyingType(value.GetType());
    switch (Type.GetTypeCode(underlyingenumtype))
    {
        case TypeCode.Int16:
        case TypeCode.Int32:
        case TypeCode.Int64:
        case TypeCode.SByte:
        case TypeCode.Single:
            {
                object obj = Activator.CreateInstance(underlyingenumtype);
                long svalue = System.Convert.ToInt64(value);
                if (svalue < 0)
                    throw new ArgumentException(
                        string.Format("Can't process negative {0} as {1}
                        enum with flags", svalue, value.GetType().Name));
            }
            break;
        default:
            break;
    }

    ulong flagsset = System.Convert.ToUInt64(value);
    Array values = Enum.GetValues(value.GetType());//.Cast<ulong />().ToArray<ulong />();
    int flagno = values.Length - 1;
    ulong initialflags = flagsset;
    ulong flag = 0;
    //start with the highest values
    while (flagno >= 0)
    {
        flag = System.Convert.ToUInt64(values.GetValue(flagno));
        if ((flagno == 0) && (flag == 0))
        {
            break;
        }
        //if the flags set contain this flag
        if ((flagsset & flag) == flag)
        {
            //unset this flag
            flagsset -= flag;
            if (flagsset == 0)
                return true;
        }
        flagno--;
    }
    if (flagsset != 0)
    {
        return false;
    }
    if (initialflags != 0 || flag == 0)
    {
        return true;
    }
    return false;
}

public static bool HasFlags(this System.Enum value)
{
    return value.GetType().GetCustomAttributes(typeof(System.FlagsAttribute),
        false).Length > 0;
}

The workhorse of the validation is the IsFlagsEnumDefined method which is a modified Enum.InternalFlagsFormat that can be obtained using the reflector.

Converting a Variable to an Enum

So far, I've been dealing with enum values that were validated after a value has been assigned. Since prevention sometimes is better than a cure, let's look at some conversion issues. If you are in a checked block and use casting for an integral value like below:

C#
checked
{
    //MultiHue has the underlying type of sbyte: -127...127
    MultiHue mh = (MultiHue)(object)128;//InvalidCastException
    int i = 128;
    mh = (MultiHue)i;//OverflowException

}

An OverflowException or InvalidCastException will be thrown if a conversion to the underlying type of the enum cannot be performed.

If the block was unchecked, there will be no exception and the mh value will become the default value of the underlying type, and that is always 0.

If you use the static Enum.Parse method and the parameter is out of bounds, you will have the OverflowException thrown regardless of the checked or uncheck mode.

C#
//MultiHue has the underlying type of sbyte: -127...127
MultiHue mh = (MultiHue)Enum.Parse(typeof(MultiHue),"128");//OverflowException

Hence I've created a method to deal with enum conversion without throwing exceptions for usual situations. Notice that converting between different enum types is not a slam dunk because you cannot take advantage of the native Enum.Parse and object's ToString methods.

C#
public static bool SafeConvertToEnum(object value, out EnumType retv)
{
    Type enumType = typeof(EnumType);
    if (!enumType.IsEnum)
        throw new System.ArgumentException(string.Format("{0} is not an Enum.",
            enumType.Name));
    if (value == null)
    {
        retv = default(EnumType);
        return false;
    }
    Type valType = value.GetType();
    bool isString = valType == typeof(string);
    bool isOrdinal = valType.IsPrimitive ||
        typeof(decimal) == valType || valType.IsEnum;
    if (!isOrdinal && !isString)
        throw new System.ArgumentException
            (string.Format("{0} can not be converted to an enum", valType.Name));

    try
    {
        checked
        {
            if (valType == Enum.GetUnderlyingType(enumType))
                retv = (EnumType)value;
            else
            {
                if(isString)
                    retv = (EnumType) Enum.Parse(typeof(EnumType), value as string);
                else
                    if (valType.IsEnum)
                    {
                        Enum en = (Enum)value;
                        object zero = Activator.CreateInstance(valType);
                        value = (en.CompareTo(zero) >= 0)?
                        Convert.ToUInt64(value):Convert.ToUInt64(value);
                    }
                retv = (EnumType)Enum.Parse(typeof(EnumType), value.ToString());
            }
        }
        if (!((System.Enum)(object)retv).IsValidEnumValue())
        {

            retv = default(EnumType);
            return false;
        }
    }
    catch(ArgumentException)
    {
        retv = default(EnumType);
        return false;
    }
    catch (OverflowException)
    {
        retv = default(EnumType);
        return false;
    }
    catch (InvalidCastException)
    {
        retv = default(EnumType);
        return false;
    }
    catch (Exception ex)
    {
        throw new System.ArgumentException(string.Format
            ("Can't convert value {0}\nfrom the type of {1}
            into the underlying enum type of {2}\nbecause {3}",
            value, valType.Name, Enum.GetUnderlyingType(enumType).Name,
                ex.Message), ex);
    }
    return true;
}

Since I've used reflection quite intensely to make this safe conversion, I think that performance was the reason why Microsoft did not make enumeration casting 'boiler plate'. The method usage should be straight forward:

C#
MultiHue mh;
//for strings
bool res = EnumHelper<multihue />.SafeConvertToEnum("3", out mh);
//for ordinal values
res = EnumHelper<multihue />.SafeConvertToEnum(3, out mh);

EnumHelper is a generic static class I've created to manipulate enums.
You will find that it contains other useful methods that you can use when working with enums using generics syntax. There is no support for enum as a generic parameter, so I've tried to use the abstract System.Enum class in the constraint clause. Since that is the base class for all of the defined enums, one would expect it should work, but unfortunately the compiler complains about it: Constraint cannot be special class 'System.Enum'.
To reach a compromise, I put the interfaces any enum implements in the where clause:

C#
public static class EnumHelper < EnumType >
where EnumType : struct, IComparable, IConvertible, IFormattable
{........

The other extension methods like GetDescription() that add syntactical sugar to the
enums are in a non generic static class called EnumExtenders.

Using the Code

In order to use this code, you have to add the namespace to your code...

C#
using CTSExtenders;

... and add a reference to CTSExtenders.dll, or better yet to the project CTSExtenders. If you don't use C# 3.0 yet, extension methods won't compile, but you can remove the 'this' parameter modifier and still use this functionality like any classic static method.

History

  • This is version 1.0.0.0

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Decebal Mihailescu is a software engineer with interest in .Net, C# and C++.

Comments and Discussions

 
GeneralHuh? What's/where is EnumType declared? Pin
PMBottas20-Jul-12 3:08
PMBottas20-Jul-12 3:08 
Generalgreat article Pin
Donsw9-Mar-09 17:16
Donsw9-Mar-09 17:16 
GeneralActually Pin
PIEBALDconsult4-Jan-08 9:30
mvePIEBALDconsult4-Jan-08 9:30 
Generalyou are right! Pin
dmihailescu4-Jan-08 11:14
dmihailescu4-Jan-08 11:14 
GeneralVery interesting Pin
Daniel Vaughan28-Dec-07 16:35
Daniel Vaughan28-Dec-07 16:35 
GeneralRe: Very interesting Pin
dmihailescu1-Jan-08 17:35
dmihailescu1-Jan-08 17:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.