§ April 4, 2007

Creating Fake Enums

In .NET enums are a strongly typed flag-like object. Flags would normally defined as a macro, or constant variable. The trouble with typical numeric flags is that a) the coder using the flag may or may not know all of the possible values, or b) might accidentally type the wrong constant or macro. Since the flag is just some magic number they may end up with a logic error that may be hard to find under certain circumstances. With enums I can provide the same logic (some numeric flag) but create a type that I can use to enforce some subset of numbers that are checked at compile time, and can be verified at run time ( Enum.IsDefined ). As with everything, enums have their faults. What if I want to extend some enum object (polymorphic enums?), or give it additional functionality or meta data. Yesterday at work we ran across a case where being able to extend an enum was mandatory. All of our enums have numeric and string values, and are typically written like:

public enum Foo {
    [FriendlyName("One Foo")]
    One = 1,
    [FriendlyName("Two Foo")]
    Two = 2,
    [FriendlyName("Three Foo")]
    Three = 3
}
the FriendlyName attribute enables you to have both a numeric flag value as well as some human readable value of the enum element other than "One" or "Two" or "SomeEnumValue"(ie: Foo.One.ToString())

Our company uses code generation like most fish use water. We use enums quite a bit and each enum type and all of their values are stored in a database which is used by the code generation tool to generate 99% of our business and data layer code. Since our code is generated, if we go in and manually add additional values that don't need to be stored in the database, our generation tool will blow away any hand written changes on every build. We would have killed to have something like a partial enum, but alas, no such thing exists.

I set out to try to create a "Fake Enum" class that acted and felt like an enum but allowed us to extend it by way of the partial class or inheritance. Well after a little poking around, inheritance flew out the window (just over complex without resorting to some generics base type), but the ability to do a partial fake enum was definitely doable.

Here's the code I came up with:

using System;
using System.ComponentModel;
using System.Globalization
using System.Reflection;

[TypeConverter(typeof(FakeEnum.FakeEnumConverter))]
public partial class FakeEnum {

    // typical "Enum" declarations, sort of like One = 1, Two = 2, Three
    public static readonly FakeEnum One = new FakeEnum(1, "One's Friendly Name");
    public static readonly FakeEnum Two = new FakeEnum(2, "Two's Friendly Name");
    public static readonly FakeEnum Three = new FakeEnum(3, "Three's Friendly Name");
    public static readonly FakeEnum Four = new FakeEnum(4);
    public static readonly FakeEnum Five = new FakeEnum(5);
    public static readonly FakeEnum Six = new FakeEnum(6);


    // implementation to provide "Enum" like functionality
    int value;
    string friendlyName;

    public string FriendlyName { get { return friendlyName; } }

    public override string ToString() {
        return ToString("");
    }

    public virtual string ToString(string format) {
        foreach(FieldInfo staticField in GetType().GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy)) {
            FakeEnum temp = staticField.GetValue(null) as FakeEnum;
            if(temp == null) continue;
            if(temp.value == value) {
                switch(format) {
                    case "fn": {
                        return temp.friendlyName;
                    }
                    default: {
                        return staticField.Name;
                    }
                }
            }
        }
        return base.ToString();
    }

    public override int GetHashCode() {
        return value.GetHashCode();
    }

    public override bool Equals(object obj) {
        if(object.ReferenceEquals(obj, null)) return false;
        FakeEnum temp = obj as FakeEnum;
        if(!object.ReferenceEquals(temp, null)) {
            return temp.value == value;
        } else {
            return false;
        }
    }

    public static object Parse(Type type, int value) {
        foreach(FieldInfo fieldInfo in type.GetFields(BindingFlags.FlattenHierarchy | BindingFlags.Static | BindingFlags.Public)) {
            FakeEnum enumValue = fieldInfo.GetValue(null) as FakeEnum;
            if(enumValue != null) {
                if(enumValue.value == value) {
                    return enumValue;
                }
            }
        }
        throw new ArgumentException(string.Format("{0} is not defined in {1}", value, type.Name));
    }

    public static object Parse(Type type, string value) {
        return Parse(type, value, false);
    }

    public static object Parse(Type type, string value, bool ignoreCase) {
        if(string.IsNullOrEmpty(value)) throw new ArgumentNullException("value was either null or empty");
        foreach(FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)) {
            bool isMatch = false;
            if(ignoreCase) {
                isMatch = StringComparer.InvariantCultureIgnoreCase.Compare(field.Name, value) == 0;
            } else {
                isMatch = StringComparer.InvariantCulture.Compare(field.Name, value) == 0;
            }
            if(isMatch) {
                FakeEnum temp = field.GetValue(null) as FakeEnum;
                if(temp == null) throw new InvalidOperationException(string.Format("{0} not convertable to {1}", type, typeof(FakeEnum)));
                object instance = Activator.CreateInstance(type, new object[] { temp.value, temp.friendlyName });
                return instance;
            }
        }
        throw new ArgumentException(string.Format("{0} is not defined in {1}", value, type.Name));
    }

    public static bool IsDefined(Type type, object value) {
        if(object.ReferenceEquals(value, null)) throw new ArgumentNullException("value");
        if(typeof(FakeEnum).IsAssignableFrom(type)) {
            return IsDefined(type, ((FakeEnum)value).value);
        }
        return false;
    }

    public static bool IsDefined(Type type, int value) {
        foreach(FieldInfo staticField in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy)) {
            object temp = staticField.GetValue(null);
            if(temp == null) continue;
            if(typeof(FakeEnum).IsAssignableFrom(type)) {
                if(((FakeEnum)temp).value == value) return true;
            }
        }
        return false;
    }

    public static bool IsDefined(Type type, string value) {
        return IsDefined(type, value, false);
    }

    public static bool IsDefined(Type type, string value, bool ignoreCase) {
        foreach(FieldInfo staticField in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy)) {
            bool isMatch = false;
            if(ignoreCase) {
                isMatch = StringComparer.InvariantCultureIgnoreCase.Compare(staticField.Name, value) == 0;
            } else {
                isMatch = StringComparer.InvariantCulture.Compare(staticField.Name, value) == 0;
            }
            if(isMatch) return true;
        }
        return false;
    }

    public static explicit operator FakeEnum(int value) {
        foreach(FieldInfo staticField in typeof(FakeEnum).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)) {
            FakeEnum temp = staticField.GetValue(null) as FakeEnum;
            if(null == temp) continue;
            if(temp.value == value) return temp;
        }
        return null;
    }

    public static explicit operator int(FakeEnum fakeEnum) {
        return fakeEnum.value;
    }

    public static implicit operator FakeEnum(string value) {
        if(string.IsNullOrEmpty(value)) return null;
        foreach(FieldInfo field in typeof(FakeEnum).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)) {
            if(StringComparer.InvariantCultureIgnoreCase.Compare(field.Name, value) == 0) {
                FakeEnum temp = field.GetValue(null) as FakeEnum;
                if( null == temp) throw new InvalidOperationException(string.Format("{0} not convertable to {1}", value, typeof(FakeEnum)));
                FakeEnum instance = new FakeEnum(temp.value, temp.friendlyName);
                return instance;
            }
        }
        return null;
    }

    public static bool operator == (FakeEnum a, FakeEnum b) { 
        if(object.ReferenceEquals(a, null)) return false;      
        return a.Equals(b);
    }

    public static bool operator != (FakeEnum a, FakeEnum b) { 
        if(object.ReferenceEquals(a, null)) return true;
        return !a.Equals(b);
    }

    protected FakeEnum(int value) : this(value, null) { }

    protected FakeEnum(int value, string friendlyName) {
        this.value = value;
        this.friendlyName = friendlyName;
        if(string.IsNullOrEmpty(friendlyName)) {
            this.friendlyName = ToString();
        }
    }

    public class FakeEnumConverter : TypeConverter {
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) {
            if(value is string) {
                return (FakeEnum)((string)value);
            }
            return base.ConvertFrom(context, culture, value);
        }
    }
}

Since we were generating these guys, we'd put the class implementation in one file, and our enum like declarations in another. It made it look a bit more "enum" like.

From there, you can use it just like a normal enum except that you now have a built in friendly name that you can access:

public void MyMethod(FakeEnum flag) {
    switch(flag) {
        case FakeEnum.One: {
            Console.WriteLine(flag.FriendlyName);
        }
    }
}
Since additional partial classes would be added, I didn't want to have to provide some functionality of ToString or explicit conversion for each partial class, so I resolved the ability to get the numeric value, string member name value through reflection. Since I had full control over the type, I decided it would be nice to add the FriendlyNameAttribute functionality into my fake enum, so I did.

Its implementation uses .net reflection to iterate over its members and obtain the enum like member and its associated and values (since every member we use like an enum will be a static instance of itself) and additionally its friendly name. actually we could make our fake enum non-numeric based (this would kind of defeat the ability to do a flag like operation) but that wasn't something needed at the time so we just stuck with int.

To be able to use these objects like a bit field (like enums are), one would need to add the | and & operators so that fields could be combined or extracted (perhaps if I'm bored I'll add this to a future version).
Posted 18 years, 5 months ago on April 4, 2007

 Comments can be posted in the forums.

© 2003 - 2024 NullFX
Creative Commons Attribution-NonCommercial-ShareAlike 3.0 License