Geeks With Blogs
// ThomasWeller C#/.NET software development, software integrity, life as a freelancer, and all the rest

NHibernate's EnumStringType is a custom user type to realize an arbitrary mapping between an enum in your domain code and a related set of strings in your database. While playing around with it, I stumbled over some strange behaviour that turned out to be a bug in NH's codebase. It took me two days to finally nail down the problem. This post is mainly intended to be a short recap of my findings. Hopefully I can prevent some other poor developer souls to get stuck with the same problem, providing them with a solution.

This story is about an enum called AddressType, which is mapped to a set of database literals, using NH's EnumStringType class to do the necessary conversions (shown here in some detail). In short, the objective was to map the enum member AddressType.MainOffice to the string "Main Office" (notice the whitespace). I did this with the following little helper class:

using System;

using NHibernate.Type;

 

namespace EnumsAndDatabase

{

    /// <summary>

    /// Custom NH datatype to map the <see cref="AddressType"/> enum

    /// to db column <c>CustomerAddress.AddressType</c>

    /// </summary>

    public class AddressTypeEnumStringType : EnumStringType<AddressType>

    {

        #region Overrides

 

        /// <summary>

        /// Gets the string value for an enum value of type <see cref="AddressType"/>.

        /// </summary>

        /// <param name="enumValue">The enum value.</param>

        /// <returns>The corresponding string value.</returns>

        public override object GetValue(object enumValue)

        {

            if (enumValue == null)

            {

                throw new ArgumentNullException("enumValue");

            }

 

            return (AddressType)enumValue == AddressType.MainOffice

                                    ? "Main Office"

                                    : base.GetValue(enumValue);

        }

 

        /// <summary>

        /// Gets the enum value of type <see cref="AddressType"/> for a string.

        /// </summary>

        /// <param name="value">The string value.</param>

        /// <returns>The corresponding enum value.</returns>

        public override object GetInstance(object value)

        {

            if (value == null)

            {

                throw new ArgumentNullException("value");

            }

 

            return (value.ToString().ToLower() == "main office")

                            ? AddressType.MainOffice

                            : base.GetInstance(value);

        }

 

        #endregion // Overrides

 

    } // class AddressTypeEnumStringType

 

} // namespace EnumsAndDatabase

This custom IUserType should do the conversion from/to AddressType.MainOffice/"Main Office" as required, when used in a mapping like the one below:

      ...

      <index column="AddressType"

             type="CustomerAddress.Persistence.NHibernateSupport.AddressTypeEnumStringType, CustomerAddress.Persistence" />

      ...

Well, at least I expected this to work...

But when I ran some tests and inspected the database afterwards, I found the string "MainOffice" instead of the expected "Main Office". I did some debugging and found out that the above GetValue() method, which should do the conversion from an enum value to its string representation, is not invoked at all. The next step was to let NH spit out some detailed log messages. Maybe the logs could help me understand what was going on here. This is what I got:

...

2009-08-27 06:26:19,131 [Task: Test Runner] DEBUG CustomerAddress.Persistence.NHibernateSupport.AddressTypeEnumStringType - binding 'Main Office' to parameter: 1

2009-08-27 06:26:19,131 [Task: Test Runner] DEBUG NHibernate.Type.Int32Type - binding '11537' to parameter: 2

2009-08-27 06:26:19,131 [Task: Test Runner] DEBUG NHibernate.SQL - INSERT INTO SalesLT.CustomerAddress (CustomerID, AddressType, AddressID) VALUES (@p0, @p1, @p2);@p0 = 12, @p1 = 'MainOffice', @p2 = 11537

...

WTF...?  Obviously, the log is stating something impossible. But by searching these log messages in the NH codebase, I was finally able to find out where this all came from. To make it short: Instead of using our overwritten GetValue() method as expected, Enum.Format() gets called on the AddressType enum to generate the SQL statement (but GetValue() is used for the log message...).

This is the relevant piece of NH code from EnumStringType.cs:

    public override void Set(IDbCommand cmd, object value, int index)

    {

        var par = (IDataParameter) cmd.Parameters[index];

        if (value == null)

        {

            par.Value = DBNull.Value;

        }

        else

        {

            par.Value = Enum.Format(ReturnedClass, value, "G");

        }

    }

Ok, now finally seeing this, it's immediately clear what the 'solution' must be: In addition to the GetValue() method, we must also overwrite the Set() method on our AddressTypeEnumStringType class. Here's the final, now working, code:

using System;

using System.Data;

using CustomerAddress.Domain;

using NHibernate.Type;

 

namespace CustomerAddress.Persistence.NHibernateSupport

{

    /// <summary>

    /// Custom NH datatype to map the <see cref="AddressType"/> enum

    /// to db column <c>CustomerAddress.AddressType</c>.

    /// </summary>

    [Serializable]

    public class AddressTypeEnumStringType : EnumStringType<AddressType>

    {

        #region Overrides

 

        /// <summary>

        /// This overwrite is actually a bugfix in NH 2.1.

        /// See <see href="http://nhjira.koah.net/browse/NH-1941">NH-1941</see>.

        /// </summary>

        public override void Set(System.Data.IDbCommand cmd, object value, int index)

        {

            var par = (IDataParameter)cmd.Parameters[index];

            if (value == null)

            {

                par.Value = DBNull.Value;

            }

            else

            {

                par.Value = this.GetValue(value);

            }

        }

 

        /// <summary>

        /// Gets the string representation for an enum value of type <see cref="AddressType"/>.

        /// </summary>

        /// <param name="enumValue">

        /// The enum value of type <see cref="AddressType"/>.

        /// </param>

        /// <returns>The corresponding string value.</returns>

        public override object GetValue(object enumValue)

        {

            if (enumValue == null)

            {

                throw new ArgumentNullException("enumValue");

            }

 

            return (AddressType)enumValue == AddressType.MainOffice

                                    ? "Main Office"

                                    : base.GetValue(enumValue);

        }

 

        /// <summary>

        /// Gets the enum value of type <see cref="AddressType"/>

        /// that corresponds to the specified string.

        /// </summary>

        /// <param name="value">The string value.</param>

        /// <returns>

        /// The corresponding enum value of type <see cref="AddressType"/>.

        /// </returns>

        public override object GetInstance(object value)

        {

            if (value == null)

            {

                throw new ArgumentNullException("value");

            }

 

            return (value.ToString().ToLower() == "main office")

                            ? AddressType.MainOffice

                            : base.GetInstance(value);

        }

 

        #endregion // Overrides

 

    } // class AddressTypeEnumStringType

 

} // namespace CustomerAddress.Persistence.NHibernateSupport

What looks easy, straightforward and somewhat trivial here, in real life took me two days of searching, debugging and googling around - not to speak of the frustration that you have if you're stuck. In the beginning, I wasn't even sure if all this was a bug or just me doing something wrong...

I submitted an issue for this bug, and I'm certain it will have vanished in the next release of NH.

After all, I have gained an important insight from this story: I could only 'fix' this error, because NH is Open Source Software. Suppose you had a log message like the one from above outputted from a Closed Source component (for which your company may have paid a considerable license fee, by the way). You then would have been forced to simply give up, looking for another way to implement the desired behaviour of your software, or to come up with an ugly hack that no one would ever really understand. But as I could read the source, I was finally able to understand the problem and to provide a solution that fits neatlessly into the software's overall architecture, not doing something strange that would make later readers/maintainers of this code shake their heads and sigh.

This is a nice example for a fact that I only felt nebulously until now: The use of Open Source software can enhance the quality of your project. I often came across the opposite attitude (It's free, so it cannot be good.), namely among management people this is not so uncommon. They're definitely wrong...

Posted on Thursday, August 27, 2009 9:20 AM NHibernate | Back to top


Comments on this post: NHibernate EnumStringType error, and praise for Open Source

# re: NHibernate EnumStringType error, and praise for Open Source
Requesting Gravatar...
If you are looking for a simple way to store these extended attributes with the enum itself, we stumbled on something interesting while doing xml serialization.

Add an System.Xml.Serialization.XmlEnumAttribute to each enumerated member with the text you want to see, and use reflection to retrieve it later.

No guarantees on performance, however.

E.g.

public enum MonkeyType {

[System.Xml.Serialization.XmlEnumAttribute("Big Bad Baboon")]
BigBadBaboon
}



using System.Reflection;
using System.Xml.Serialization;

...

public string GetXmlEnumAttribute(Enum value)
{
string description = string.Empty;

Type enumType = value.GetType();
Type xmlEnumType = typeof(XmlEnumAttribute);

FieldInfo fi = enumType.GetField(Enum.GetName(enumType, value));

if (fi != null)
{
if (fi.IsDefined(xmlEnumType, false))
{
object[] attributes = fi.GetCustomAttributes(xmlEnumType, false);
if (attributes.Length > 0)
{
XmlEnumAttribute attribute = attributes[0] as XmlEnumAttribute;
if (attribute != null)
{
description = attribute.Name;
}
}
}
}

return description;
}

Left by Nic on Oct 19, 2009 5:39 PM

# re: NHibernate EnumStringType error, and praise for Open Source
Requesting Gravatar...
Thank you for an excellent write up on an elusive issue. Saved my butt.
Left by Jace B on Oct 24, 2009 5:32 PM

# re: NHibernate EnumStringType error, and praise for Open Source
Requesting Gravatar...
This is a cool solution and solved me a lot of troubles. I am working with the latest version 1.2.400 GA and the problem still persists.
Cool, thanks!
Left by raffaeu on Nov 04, 2009 5:04 PM

# re: NHibernate EnumStringType error, and praise for Open Source
Requesting Gravatar...
Nice to see that this helped you out, that's what I actually intended with this post.
But the most recent version of NHibernate is 2.1.1., and the issue is resolved in this version...
Left by Thomas Weller on Nov 04, 2009 5:25 PM

Your comment:
 (will show your gravatar)


Copyright © Thomas Weller | Powered by: GeeksWithBlogs.net