Enum, Alternate Values, and FluentNhibernate






4.33/5 (3 votes)
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 enum
s, 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:
/// <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.
/// <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:
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 enum
s, via a call to GetKeyDictionaryForEnum
. GetKeyDictionaryForEnum
checks if the Keys
are loaded. If they aren't loaded, GetKeyDictionaryForEnum
loads them via LoadKeys
.
/// <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.
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:
public static T EnumParseKey<T>(this T parseEnum,
string key, bool ignoreCase,
bool ignoreLeadingTrailingSpaces)
which results in a call like:
KeyedEnum retrieved = testEnum.EnumParseKey<KeyedEnum>(key, false, false);
Usable, but a little awkward looking.
So we ended up with the following EnumParseKey
method:
/// <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:
/// <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.
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