Sunday, February 1, 2015

Generalized Enumerations in C#

See the full source code on CodePlex.

Introduction

Did it ever bother you that your enumerations can have only integers as their base types? It sure did bother me!
When working with the database, it happens often to have cryptic strings as values, for example ‘C’ for ‘Closed’, ‘O’ for ‘Open’ etc. It would be nice to have some enumeration that can be based on that string, with a proper symbolic name, and a nice label for the UI.
You can’t get this with a regular enumeration, but that shouldn’t stop you. If you think about it, an enumeration is a class that exposes just a few public public static readonly values.
Here’s a very simple piece of code:
public abstract class Enumerated<TBase>
{
    // Actual value
    public TBase Value { get; private set; }

    // Label for the UI.
    public string Label { get; private set; }

    protected Enumerated(string label, TBase value)
    {
        this.Label = label;
        this.Value = value;
    }
}

class Status : Enumerated<string>
{
    public static readonly Status Open = new Status("open", "O");
    public static readonly Status Closed = new Status("closed", "C");

    private Status(string label, string value) : base(label, value) { }
}
We can now use the values just like we would an enumerated type:
Status status = Status.Open;

ToString

So far, our base type doesn’t bring too much value, but we’ll keep adding functionality. For example, now if we try to print a value, we’ll get:
Namespace.Status
which is not very useful. We’ll override ToString to print the label instead:
public override string ToString()
{
    return Label;
}
This will come in handy in a lot of places, including the UI.

Enumerating All the Values

It is often the case in UIs that you want to allow the user to choose from a list of all possible values. With a proper enum, you can always call Enum.GetValues to get this list. Let’s implement something similar for our type.
Of course, we could create a list by hand in every derived class, but that’s not very convenient. So we’ll use reflection to get a list of all the public static readonly fields (i.e., variables, not properties) of our type.
public static IEnumerable<?> Values { get; }
Hmm, what type should there be in our list? We don’t really have a way of knowing this from the base type. To work around this limitation, we will add a second type parameter to our generic class. This may seem a bit strange, but it gets the job done.
public abstract class Enumerated<TThis, TBase>
To use it, you would do this:
class Status : Enumerated<Status, string>
Getting all the values is relatively straightforward:
private static IEnumerable<TThis> values = InitValues();

private static IEnumerable<TThis> InitValues()
{
    List<TThis> list = new List<TThis>();

    // Look for all non-inherited fields (that is, member variables) that are public and static.
    FieldInfo[] staticFields = typeof(TThis).GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static);
    foreach (FieldInfo field in staticFields)
    {
        // Ignore fields that aren't of the correct type or read-only.
        if (field.FieldType != typeof(TThis) || (field.Attributes & FieldAttributes.InitOnly) == (FieldAttributes)0)
            continue;

        // Retrieve the value of the field. No object is required, because it's a static field.
        TThis value = (TThis)field.GetValue(null);

        // Store it in the list for later.
        list.Add(value);
    }
    return list;
}

public static IEnumerable<TThis> Values
{
    get
    {
        return values;
    }
}

Debugging Enhancements

While we’re here, it would be nice if we could also retrieve the name of the field automatically. For this, we need to add a new property:
// Name of the field, as used in code.
public string Name { get; private set; }
And we will fill it in InitValues:
// Save the field's name, as defined in code.
value.Name = field.Name;
The compiler complains about our use of Name:
error CS1061: 'TThis' does not contain a definition for 'Name' and no extension method 'Name' accepting a first argument of type 'TThis' could be found (are you missing a using directive or an assembly reference?)
Which makes sense, seeing that the compiler only knows that TThis is an object.
We’ll let the compiler know that TThis is actually derived from Enumerated:
public abstract class Enumerated<TThis, TBase>
    where TThis : Enumerated<TThis, TBase>
If you look at the value in the debugger, all you’ll see is {open}, which is what ToString returns. You can change this using the DebuggerDisplay attribute.
[DebuggerDisplay("{Name}")]
public abstract class Enumerated<TThis, TBase>
    where TThis : Enumerated<TThis, TBase>
Or, if you want more information, you can try:
[DebuggerDisplay("{Name} / {Value} / {Label}")]

Conversion

Using a regular enum, you can convert from the integer type to your enumeration and vice versa, using the cast operator:
MyEnumeration x = (MyEnumeration)1;
int i = (int)MyEnumeration.A;
Let’s do the same thing for our Enumerated types:
Conversion to TBase is easy:
public static explicit operator TBase(Enumerated<TThis, TBase> obj)
{
    return obj.Value;
}
The other way around is a bit more complicated. We need to look the base value and return the proper object of our type. This is job for a Dictionary.
We will adapt our values accordingly:
private static IDictionary<TBase, TThis> values = InitValues();

public static IEnumerable<TThis> Values
{
    get
    {
        return values.Values;
    }
}
Of course, InitValues also needs to be adapted:
private static IDictionary<TBase, TThis> InitValues()
{
    var list = new Dictionary<TBase, TThis>
    // ...
    list.Add(value.Value, value);
    // ...
}
Now we can implement the conversion operator:
public static explicit operator Enumerated<TThis, TBase>(TBase obj)
{
    return values[obj];
}
This is certainly simple, but if we try to convert a value that isn’t valid for our enumeration, we get a KeyNotFoundException, instead of the InvalidCastException that we would expect. Let’s fix this:
public static explicit operator Enumerated<TThis, TBase>(TBase obj)
{
    try
    {
        return values[obj];
    }
    catch (KeyNotFoundException ex)
    {
        // Expected error: the code is unknown. In this case, say 'invalid value' rather than the cryptic, implementation-oriented 'key not found'.
        var errorMessage = string.Format("Conversion from {0} to {1} failed: '{2}' is not a valid value.", typeof(TBase), typeof(TThis), obj);
        throw new InvalidCastException(errorMessage, ex);
    }
}

XML Serialization

Assuming that your enumerated type is public, and it has a parameterless constructor (it can be private, so that only the system uses it), you can use an XmlSerializer to write it. However, this would just list all the public properties, which is probably not what you want.
If you derive your type from Enumerated, then it won't work at all, because you have some public properties that cannot be set, and XmlSerializer doesn’t like it.
Instead, you’ll want Enumerated to implement IXmlSerializable. Then you’ll need to implement 3 functions: GetSchema (this can just return null), ReadXml (we’ll get to it a bit later), and WriteXml. The latter is quite easy:
void IXmlSerializable.WriteXml(XmlWriter writer)
{
    writer.WriteString(Name);
}
As you can see, I want the Value in the database, but the Name in the XML. Feel free to change this.
At this, point, you might expect that the following code:
var serializer = new XmlSerializer(typeof(Status));
var builder = new StringBuilder();
using (var writer = new StringWriter(builder))
{
    serializer.Serialize(writer, Status.Open);
}
Debug.Print("{0}", builder);
will print:
<?xml version="1.0" encoding="utf-16"?>
<Status>Open</Status>
But instead you will get:
<?xml version="1.0" encoding="utf-16"?>
<Status />
It seems that the static members were not initialized because they were never used.
You might think that it would help to just call InitValues from the static constructor of Enumerated, but that would actually not work, because it will be called before the derived class is initialized. Same for the instance constructor.
Instead, we’ll have the values either when Name or Values is first accessed. I will also skip a step and index the names too; they will be needed later.
private string name;

public string Name {
    get
    {
        InitValues();
        return name;
    }
    private set
    {
        name = value;
    }
}

public static IEnumerable<TThis> Values
{
    get
    {
        InitValues();
        return values.Values;
    }
}

private static IDictionary<TBase, TThis> values;
private static IDictionary<string, TThis> names;

private static void InitValues()
{
    if (values != null)
        return;

    var newValues = new Dictionary<TBase, TThis>();
    var newNames = new Dictionary<string, TThis>();

    // Look for all non-inherited fields (that is, member variables) that are public and static.
    FieldInfo[] staticFields = typeof(TThis).GetFields(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static);
    foreach (FieldInfo field in staticFields)
    {
        // Ignore fields that aren't of the correct type or read-only
        if (field.FieldType != typeof(TThis) || (field.Attributes & FieldAttributes.InitOnly) == (FieldAttributes)0)
            continue;

        // Retrieve the value of the field. No object is required, because it's a static field.
        TThis value = (TThis)field.GetValue(null);

        // Save the field's name, as defined in code.
        // Use the field rather than the property in order to prevent recursion.
        value.name = field.Name;

        // Store it in the list for later.
        newValues.Add(value.Value, value);
        newNames.Add(value.name, value);
    }
    values = newValues;
    names = newNames;
}
For ReadXml we just need to get the Name from the XmlReader and look it up in our dictionary:
void IXmlSerializable.ReadXml(XmlReader reader)
{
    var name = reader.ReadElementContentAsString();
    var value = FromName(name);
    this = value;
}

TThis FromName(string name)
{
    try
    {
        return names[name];
    }
    catch (KeyNotFoundException ex)
    {
        // Say 'invalid value' rather than the cryptic, implementation-oriented 'key not found'.
        var errorMessage = string.Format("There is no {0} value with the name '{1}'.", typeof(TThis), name);
        throw new InvalidCastException(errorMessage, ex);
    }
}
This would work if this were a struct. However, structs are rather limited in .NET, so we have to use a reference type and we cannot change the this reference.
Instead, we’ll emulate this with a function call:
void IXmlSerializable.ReadXml(XmlReader reader)
{
    var name = reader.ReadElementContentAsString();
    var value = FromName(name);
    this.Assign(value);
}

protected virtual void Assign(Enumerated<TThis, TBase> other)
{
    this.Name = other.Name;
    this.Value = other.Value;
    this.Label = other.Label;
}
Assign is a virtual function, in case your type has more properties.
Now the XML deserialization creates a perfect copy of our static value... except that testing for equality fails! That’s because the default comparison just compares references. Fortunately, it’s easy to change.
public override bool Equals(object obj)
{
    if (obj == null)
        return false;
    else if (obj.GetType() != this.GetType())
        return false;
    return object.Equals(((Enumerated<TThis, TBase>)obj).Value, Value);
}

public static bool operator==(Enumerated<TThis, TBase> left, Enumerated<TThis, TBase> right)
{
    return object.Equals(left, right);
}

public static bool operator !=(Enumerated<TThis, TBase> left, Enumerated<TThis, TBase> right)
{
    return !object.Equals(left, right);
}
Now the compiler complains that we did not also override GetHashCode. The one from our value seems like the obvious choice.
public override int GetHashCode()
{
    return Value.GetHashCode();
}

In Method

It is sometimes nice to be able to write something like this:
if (status.In(Status.Open, Status.Closed))
We will cover both the case when you have a variable number of parameters and the one where you just pass a pre-existing list:
public virtual bool In(params TThis[] values)
{
    return In((IList<TThis>)values);
}

public virtual bool In(IList<TThis> values)
{
    return values.Contains((TThis)this);
}

What Now?

There are still a number of things left to do:
  • Make the initialization thread safe.
  • Support localization of the Label.
  • Add a type editor / converter for the Visual Studio designer.
This is left, as they say, as an exercise to the reader.

See the full source code on CodePlex.