Click here to Skip to main content
15,867,704 members
Articles / Programming Languages / C#

Enum, Alternate Values, and FluentNhibernate

Rate me:
Please Sign up or sign in to vote.
4.33/5 (3 votes)
29 May 2011CPOL6 min read 23.3K   303   10   3
Pulling together various techniques to make enums, alternate values, and FluentNHibernate play well together

Introduction

As I started to use FluentNHibernate, I found I needed a way to use enums, create readable code, and persist string values associated to the enum.

For example, let's say we have an enum of U.S. States. I would want to use the enum to enforce only being able to use certain values, the States. I would like the code to be readable, like 'if (mystate == EnumState.Alabama)' as opposed to something illegible like 'if (mystate == 1)'. I would also like the State abbreviation stored, instead of the full state name, like 'AL', or a number like 1.

Another example would be process status where you have an enum representing each status. We'd like to be able to perform checks like 'if (process.Status == EnumStatus.Started)'. For various reasons, it was decided to persist 'S', not 'Started' or 1 in the database.

To handle this, I found a few good blog postings and samples and I figured pulling them together may help.

Using the Code

The pieces we needed included attributes, extensions, and create an nHibernate IUserType. The attributes would provide us an easy way to add our alternative value to the enum. The extensions would make it easy to work with the attributes. The IUserType will provide us an easy way to map with FluentNHibernate the alternate value.

Attributes

To code provided uses two attributes, EnumKeyAttribute and EnumDescriptionAttribute. Article Enum Databinding provides a few examples on using an attribute. The code will use the EnumKeyAttribute to create an alternate value. EnumDescriptionAttribute is similar to the article and provides a way to have a description of the enum value available. EnumDescriptionAttribute could be used to populate a list or drop down for example.
The following provides an example enum used for testing:

C#
/// <summary>
/// enum to test saving by a key
/// </summary>
public enum KeyedEnum
{
    [EnumKey("Alpha")]
    [EnumDescription("First Position")]
    first,
    [EnumKey("Beta")]
    [EnumDescription("Second Position")]
    second,
    [EnumDescription("Second Position")]
    missingKey,
    [EnumKey("Gamma")]
    missingDescription
}

As seen above, it's fairly easy to declare the attributes and set the values. The functionality of the enum hasn't changed.

Extension

With the attributes in place, we can use the attributes with extensions to give us more functionality. What we'll see later is we really need a function to retrieve an enum value from a string holding a Key and a function to return a Key associate to the current value of an enum. We also want the functions to work fast as we'll be using them to persist values. Now reading attributes usually involves using Reflection. So to improve performance, the EnumExtension reads the attributes and stores them in a Dictionary. The Dictionary is stored in a static HybridDictionary based on the enum's full name. Now, to not cause a huge delay at startup, the Dictionary and HybridDictionary will be populated for the one enum when extension methods needing the attribute values are first called. Hence, there's only one Reflection hit to retrieve the values for each enum.

Below provides a quick look at the GetKey extension to support retrieving a Key set by the EnumKeyAttribute for an enum's value.

C#
/// <summary>
/// Gets the <see cref="EnumKeyAttribute" /> of an <see cref="Enum" /> 
/// type value.
/// </summary>
/// <param name="value">The <see cref="Enum" /> type value.</param>
/// <returns>A string containing the text of the
/// <see cref="EnumKeyAttribute"/>.</returns>
public static string GetKey(this Enum value)
{
    if (value == null)
    {
        throw new ArgumentNullException("value");
    }

    Type enumType = value.GetType();

    //retrieve enumDictionary from static
    Dictionary<string, string> enumDict = GetKeyDictionaryForEnum(enumType);
    return enumDict[value.ToString()];
}

As a little note, the above code is not allowing an enum to have a null value.

With the GetKey extension, we can now right code like:

C#
KeyEnum testEnum = KeyEnum.first;
string actual = testEnum.GetKey();

'actual' would now contain the Key associated to the enum value of 'testEnum'. For the example enum from above, 'actual' should now contain 'Alpha'.
EnumExtensionTests provide more examples.

GetKey and other methods looking for Key first try to retrieve the Keys Dictionary for the enum from '_keys', the static HybridDictionary holding the Keys for enums, via a call to GetKeyDictionaryForEnum. GetKeyDictionaryForEnum checks if the Keys are loaded. If they aren't loaded, GetKeyDictionaryForEnum loads them via LoadKeys.

C#
/// <summary>
/// For an enum, retrieve its Dictionary of Key's
/// Uses full name of the enum.
/// </summary>
/// <param name="enumType"></param>
/// <returns></returns>
private static Dictionary<string, string> GetKeyDictionaryForEnum(Type enumType)
{
    Dictionary<string, string> enumDict 
               = (Dictionary<string, string>)_keys[enumType.FullName];
    if (enumDict == null)
    {
        //most likely not loaded, so need to load
        LoadKeys(enumType);
        enumDict = (Dictionary<string, string>)_keys[enumType.FullName];
    }
    return enumDict;
} 

Having the GetKeyDictionaryForEnum makes it easy to include in other methods that need the Key dictionary. The GetKeyDictionaryForEnum does call LoadKeys which is a slightly more intense method.

C#
private static void LoadKeys(Type enumType)
{
    //check if have _values loaded
    if (_keys.Contains(enumType.FullName) == false)
    {
        lock (_lockObject)
        {
            //now have locked, check if somebody else still put value in
            if (_keys.Contains(enumType.FullName) == false)
            {
                //load up
                Dictionary<string, string> enumDict = new Dictionary<string, string>();
                //get values
                //foreach (string name in Enum.GetNames(enumType))
                //{

                 //   FieldInfo fieldInfo = enumType.GetField(name);
                const string fieldValueName = "value__";
                foreach (FieldInfo fieldInfo in enumType.GetFields()) 
                                                  //couldn't use binding
                {
                    if (fieldInfo.Name != fieldValueName) //hack
                    {

                        EnumKeyAttribute[] attributes 
                                   = (EnumKeyAttribute[])
                            fieldInfo.GetCustomAttributes(typeof
                                                (EnumKeyAttribute), false);

                        if (attributes != null && attributes.Length > 0)
                        {
                            string description = attributes[0].Description;
                            //enumDict.Add(name, description);
                            enumDict.Add(fieldInfo.Name, description);
                        }
                        else
                        {
                            //if don't have a key attribute,
                            // load with field name
                            //enumDict.Add(name, name);
                            enumDict.Add(fieldInfo.Name, fieldInfo.Name);
                        }
                    }
                }
                _keys.Add(enumType.FullName, enumDict);
            }
        }
    }
}

As this method is static, we have to be conscientious that more than one thread may be calling it or accessing the _keys attribute. Hence at the beginning, the code has the usual locking calls. Once locked, the code loops through the values on the enum looking for the EnumKeyAttribute. One field proved to be a little troublesome, the 'value__' field. Hence, the hack to look for it as we are not concerned with it in this code. Another point of interest in the LoadKeys code is the usage of the value's name if a EnumKeyAttribute is not placed on a value. Also, note the code does not check if the Key with the same value is loaded. This check could be added, unfortunately the check would happen at runtime. In this case, the usage of the same Key was allowed and the code looks for the first instance of the Key in the Dictionary when searching for it to retrieve a value.

Now we also need a function to retrieve an enum with its value set based on a string holding a Key. There are several ways to do this, like adding an extension to string (there's an example of this in the sample code). In this case, it was decided to use a method on the EnumExtension class. Unfortunately with extension, you can't write
'somemethod (this enum myenum)' with C# 3.5. One way to make it work is to declare an extension as:

C#
public static T EnumParseKey<T>(this T parseEnum,
                                 string key, bool ignoreCase,
                                 bool ignoreLeadingTrailingSpaces)

which results in a call like:

C#
KeyedEnum retrieved = testEnum.EnumParseKey<KeyedEnum>(key, false, false);

Usable, but a little awkward looking.

So we ended up with the following EnumParseKey method:

C#
/// <summary>
/// Uses the Key to return the first enum with the supplied key.
/// Key may have a space in the front or back
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="ignoreCase">if true will not trim off spaces</param>
/// <returns></returns>
public static T EnumParseKey<T>(string key,
                            bool ignoreCase, bool ignoreLeadingTrailingSpaces)
{
    if (String.IsNullOrEmpty(key))
    {
        throw new ArgumentNullException("key","key is null or empty");
    }

    if (!ignoreLeadingTrailingSpaces)
    {
        key = key.Trim();

        if (key.Length == 0)
        {
            throw new ArgumentException("Must specify.....", "key");
        }
    }

    Type type = typeof(T);

    if (!type.IsEnum)
    {
        throw new ArgumentException("Type provided must be an enum.", "T");
    }

    Array enumValues = Enum.GetValues(type);

    foreach (Enum enumVal in enumValues)
    {
        if (GetKey(enumVal).CompareTo(key) == 0)
            return (T)Enum.Parse(type, enumVal.ToString(), ignoreCase);
    }
    throw new ArgumentException(string.Format("enum key ({0}) not found.",key), "key");
}

The function performs a few checks on the key string. Attempts to return some decent error messages. Following the checks, EnumParseKey loops through the values to find a matching Key. This part of the code could probably be improved for performance, but it assumes an enum won't have many values.

So we now have our two main functions for working with Keys, time for the code that will use them.

IUserType

nHibernate provides a way to create custom types. Luckily, I found a post at this link where DaeMoohn posted an IUserType that practically matched what I needed to map with the Key.
After a little bit of tweaking, the goal became obtainable. From EnumKeyMapKeyHelper, we have:

C#
/// <summary>
/// Using the data on the data reader, return an enum with the value set.
/// </summary>
/// <param name="rs"></param>
/// <param name="names"></param>
/// <param name="owner"></param>
/// <returns></returns>
public object NullSafeGet(System.Data.IDataReader rs, string[] names, object owner)
{
    int index0 = rs.GetOrdinal(names[0]);
    if (rs.IsDBNull(index0))
    { return null; }
    string key = rs.GetString(index0);
    return EnumExtension.EnumParseKey<T>(key, false, true);
}
/// <summary>
/// Using Value of enum, find the key, and place the key
/// </summary>
/// <param name="cmd"></param>
/// <param name="value"></param>
/// <param name="index"></param>
public void NullSafeSet(System.Data.IDbCommand cmd, object value, int index)
{
    if (value == null)
    {
        ((IDbDataParameter)cmd.Parameters[index]).Value = DBNull.Value;
    }
    else
    {
        T enumValue = (T)Enum.Parse(typeof(T), value.ToString());
        
        ((IDbDataParameter)cmd.Parameters[index]).Value = enumValue.GetKey<T>();
    }
} 

These functions are using EnumParseKey and GetKey to return the appropriate values for nHibernate to map. Once in place, it's easy with FluentNHibernate to make the map.

C#
Map(x => x.KeyedEnumKey, "KeyedEnumKey")

                          .CustomType<EnumKeyMapKeyHelper<KeyedEnum>>().Not.Nullable();

That's it. The Key from the EnumKeyAttribute will now be persisted by nHibernate.

Points of Interest

The code provided has many unit tests for various scenarios. It even has scenarios for mapping the Description.
If using the sample code, be sure to change the path to local SQL database.
There may be a few areas for improvement or touching up for your environment, but big thanks to the other posters to give us a starting point.

History

  • 29th May, 2011: Initial post

License

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


Written By
Team Leader
United States United States
A biography in this little spot...sure.
I've worked at GTE HawaiianTel. I've worked at Nuclear Plants. I've worked at Abbott Labs. I've consulted to Ameritech Cellular. I've consulted to Zurich North America. I've consulted to International Truck and Engine. Right now, I've consulted to Wachovia Securities to help with various projects. I've been to SHCDirect and now at Cision.

During this time, I've used all kinds of tools of the trade. Keeping it to the more familier tools, I've used VB3 to VB.NET, ASP to ASP/JAVASCRIPT/XML to ASP.NET. Currently, I'm developing with C# and ASP.NET. I built reports in Access, Excel, Crystal Reports, and Business Objects (including the Universes and ETLS). Been a DBA on SQL Server 4.2 to 2000 and a DBA for Oracle. I've built OLTP databases and DataMarts. Heck, I've even done Documentum. I've been lucky to have created software for the single user to thousands of concurrent users.

I consider myself fortunate to have met many different people and worked in many environments. It's through these experiences I've learned the most.

Comments and Discussions

 
GeneralNHibernate can by default handle Enums without any problems Pin
JV999929-May-11 21:06
professionalJV999929-May-11 21:06 
GeneralRe: NHibernate can by default handle Enums without any problems Pin
Tim Schwallie30-May-11 12:50
Tim Schwallie30-May-11 12:50 
GeneralRe: NHibernate can by default handle Enums without any problems Pin
JV999930-May-11 20:29
professionalJV999930-May-11 20:29 
Fair enough, I do know a very few scenarios in which the cost of creating a reference table would be higher then storing it in code (which normally is more costly due to deployment/change reasons) so I understand your point. All in all, your articles definiatly is a good example for IUserType.

Gave you a 4. Would have given you a 5 if you would have given a bit more info on FluentNHibernate than just the example mapping Smile | :) .

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.