Using Enum types with NHibernate (implementing IUserType)

Friday, 9 November 2007

NHibernate is a really nice ORM framework, and one of its features is that you can customise the mapping between the business entity’s properties and database columns.

For example, you might have a ‘GENDER’ column in a database table that has the values “M” or “F”.

Rather than your entity class have a String or Char property, wouldn’t it be nicer to use a custom Enum like this?

EntityObj.Gender = Sex.Male

In order to implement this functionality, there are a few steps that need to be completed.

StringValueAttribute

We need some way of recording the string equivalent for each enum value. One way to do this is to use a custom attribute like this:

Public NotInheritable Class StringValueAttribute

    Inherits System.Attribute

    Private \_value As String

    Public Sub New(ByVal value As String)

        \_value = value

    End Sub

    Public ReadOnly Property Value() As String

        Get

            Return \_value

        End Get

    End Property

End Class

You apply this to each value of the enumerated type:

Public Enum Sex

    <StringValue("M")> Male

    <StringValue("F")> Female

End Enum

TypeConverters

Next, we need a nice way to read those values and convert between the enumerated type and the string value. A custom TypeConverter can do this.

Here’s a generic TypeConverter class:

Imports System.ComponentModel

Imports System.Globalization

''' <summary>

''' A generic base class for implementing type converters for enumerations.

''' </summary>

''' <typeparam name="t"></typeparam>

''' <remarks>The Enumerations must use the <see cref="StringValueAttribute">StringValueAttribute</see> attribute

''' to map the string/character value with each enumerated value.</remarks>

Public Class EnumConverter(Of t As Structure)

    Inherits EnumConverter

    Public Sub New()

        MyBase.New(GetType(t))

    End Sub

    ''' <summary>

    ''' We can convert from String or Char

    ''' </summary>

    ''' <param name="context"></param>

    ''' <param name="sourceType"></param>

    ''' <returns></returns>

    ''' <remarks></remarks>

    Public Overrides Function CanConvertFrom(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal sourceType As System.Type) As Boolean

        If sourceType Is GetType(String) OrElse sourceType Is GetType(Char) Then

            Return True

        Else

            Return False

        End If

        'Return MyBase.CanConvertFrom(context, sourceType)

    End Function

    ''' <summary>

    ''' Convert from String and Char

    ''' </summary>

    ''' <param name="context"></param>

    ''' <param name="culture"></param>

    ''' <param name="value"></param>

    ''' <returns></returns>

    ''' <remarks>If it is a comma-separated list, then will combine values</remarks>

    Public Overrides Function ConvertFrom(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal culture As System.Globalization.CultureInfo, ByVal value As Object) As Object

        If TypeOf value Is String OrElse TypeOf value Is Char Then

            'Dim x As t

            Dim rv As Integer

            Dim strValue As String = CStr(value).ToUpper(CultureInfo.CurrentCulture)

            For Each ch As String In strValue.Split(","c)

                ch = ch.Trim()

                Dim FieldInfos() As System.Reflection.FieldInfo = GetType(t).GetFields(Reflection.BindingFlags.Static Or Reflection.BindingFlags.Public) ' ft.GetType().GetField(value.ToString())

                For i As Integer = 0 To FieldInfos.Length - 1

                    Dim fi As Reflection.FieldInfo = FieldInfos(i)

                    Dim attributes() As StringValueAttribute = CType(fi.GetCustomAttributes(GetType(StringValueAttribute), False), StringValueAttribute())

                    If attributes.Length > 0 AndAlso attributes(0).Value = ch Then

                        Dim newValue As Integer = CInt(System.Enum.Parse(GetType(t), fi.Name))

                        rv = rv Or newValue

                    End If

                Next

            Next

            ' a bit messy, but we cast to an object then back to the t type

            Return CType(CType(rv, Object), t)

        Else

            Throw New NotSupportedException()

        End If

    End Function

    ''' <summary>

    ''' We can convert to String and Char

    ''' </summary>

    ''' <param name="context"></param>

    ''' <param name="destinationType"></param>

    ''' <returns></returns>

    ''' <remarks></remarks>

    Public Overrides Function CanConvertTo(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal destinationType As System.Type) As Boolean

        If destinationType Is GetType(String) OrElse destinationType Is GetType(Char) Then

            Return True

        Else

            Throw New NotSupportedException()

        End If

    End Function

    ''' <summary>

    ''' Convert to String or Char

    ''' </summary>

    ''' <param name="context"></param>

    ''' <param name="culture"></param>

    ''' <param name="value"></param>

    ''' <param name="destinationType"></param>

    ''' <returns></returns>

    ''' <remarks></remarks>

    Public Overrides Function ConvertTo(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal culture As System.Globalization.CultureInfo, ByVal value As Object, ByVal destinationType As System.Type) As Object

        If value Is Nothing Then

            Throw New ArgumentNullException("value")

        End If

        Dim ch As String = String.Empty

        Dim ft As t = CType(value, t)

        Dim fi As System.Reflection.FieldInfo = ft.GetType().GetField(value.ToString())

        Dim attributes() As StringValueAttribute = CType(fi.GetCustomAttributes(GetType(StringValueAttribute), False), StringValueAttribute())

        If attributes.Length > 0 Then

            If destinationType Is GetType(String) Then

                Return attributes(0).Value

            ElseIf destinationType Is GetType(Char) Then

                Return CChar(attributes(0).Value)

            Else

                'Return MyBase.ConvertTo(context, culture, value, destinationType)

                Throw New NotSupportedException()

            End If

        Else

            Throw New ArgumentException("StringValue attribute not found for this value")

        End If

    End Function

End Class

You apply this to the enumerated type:

Imports System.ComponentModel

<TypeConverter(GetType(EnumConverter(Of Sex)))> \_

Public Enum Sex

    <StringValue("M")> Male

    <StringValue("F")> Female

End Enum

Implementing IUserType

You now need a class that implements IUserType. This class is used by NHibernate to do the mapping/conversion to and from the database to the property on the class.

Here’s a generic version, that you can then inherit from for your specific type:

Imports NHibernate

Imports NHibernate.UserTypes

''' <summary>

''' Generic class for implementing NHibernate's <see cref="IUserType">IUserType</see> for a value type.

''' </summary>

''' <typeparam name="T">Value type</typeparam>

''' <remarks>The value type's <see cref="System.ComponentModel.TypeConverter">TypeConverter</see> is used for conversion</remarks>

Public MustInherit Class ValueConverterUserType(Of T As Structure)

    Implements IUserType

    Protected Converter As ComponentModel.TypeConverter = ComponentModel.TypeDescriptor.GetConverter(GetType(T))

    ''' <summary>

    ''' Reconstruct an object from the cacheable representation.

    ''' </summary>

    ''' <param name="cached"></param>

    ''' <param name="owner"></param>

    ''' <returns></returns>

    ''' <remarks>At the very least this method should perform a deep copy if the type is mutable. (optional operation)</remarks>

    Public Function Assemble(ByVal cached As Object, ByVal owner As Object) As Object Implements NHibernate.UserTypes.IUserType.Assemble

        Return DeepCopy(cached)

    End Function

    ''' <summary>

    ''' Return a deep copy of the persistent state, stopping at entities and at collections.

    ''' </summary>

    ''' <param name="value"></param>

    ''' <returns></returns>

    ''' <remarks></remarks>

    Public Function DeepCopy(ByVal value As Object) As Object Implements NHibernate.UserTypes.IUserType.DeepCopy

        Return value

    End Function

    ''' <summary>

    ''' Transform the object into its cacheable representation.

    ''' </summary>

    ''' <param name="value"></param>

    ''' <returns></returns>

    ''' <remarks>At the very least this method should perform a deep copy if the type is mutable.

    ''' That may not be enough for some implementations, however; for example, associations must be cached as identifier values. (optional operation)</remarks>

    Public Function Disassemble(ByVal value As Object) As Object Implements NHibernate.UserTypes.IUserType.Disassemble

        Return DeepCopy(value)

    End Function

    ''' <summary>

    ''' Compare two instances of the class mapped by this type for persistent "equality" ie. equality of persistent state

    ''' </summary>

    ''' <param name="x"></param>

    ''' <param name="y"></param>

    ''' <returns></returns>

    ''' <remarks></remarks>

    Public Shadows Function Equals(ByVal x As Object, ByVal y As Object) As Boolean Implements NHibernate.UserTypes.IUserType.Equals

        Return x.Equals(y)

    End Function

    ''' <summary>

    ''' Get a hashcode for the instance, consistent with persistence "equality"

    ''' </summary>

    ''' <param name="x"></param>

    ''' <returns></returns>

    ''' <remarks></remarks>

    Public Shadows Function GetHashCode(ByVal x As Object) As Integer Implements NHibernate.UserTypes.IUserType.GetHashCode

        Return x.GetHashCode()

    End Function

    ''' <summary>

    ''' Are objects of this type mutable?

    ''' </summary>

    ''' <value></value>

    ''' <returns></returns>

    ''' <remarks></remarks>

    Public ReadOnly Property IsMutable() As Boolean Implements NHibernate.UserTypes.IUserType.IsMutable

        Get

            Return False

        End Get

    End Property

    ''' <summary>

    ''' Retrieve an instance of the mapped class from an <see cref="IDataReader">IDataReader</see>.

    ''' </summary>

    ''' <param name="rs">IDataReader</param>

    ''' <param name="names">Column names</param>

    ''' <param name="owner">The containing entity</param>

    ''' <returns></returns>

    ''' <remarks></remarks>

    Public Function NullSafeGet(ByVal rs As System.Data.IDataReader, ByVal names() As String, ByVal owner As Object) As Object Implements NHibernate.UserTypes.IUserType.NullSafeGet

        Dim obj As Object = NHibernateUtil.String.NullSafeGet(rs, names)

        Dim s As String = CStr(obj)

        Return Converter.ConvertFromString(s)

    End Function

    ''' <summary>

    ''' Write an instance of the mapped class to a prepared statement.

    ''' </summary>

    ''' <param name="cmd">an IDbCommand</param>

    ''' <param name="value">the object to write</param>

    ''' <param name="index">command parameter index</param>

    ''' <remarks>Implementors should handle possibility of null values.  A multi-column type should be written to parameters starting from index.</remarks>

    Public Sub NullSafeSet(ByVal cmd As System.Data.IDbCommand, ByVal value As Object, ByVal index As Integer) Implements NHibernate.UserTypes.IUserType.NullSafeSet

        Dim NativeValue As String = Converter.ConvertToString(value)

        NHibernateUtil.String.NullSafeSet(cmd, NativeValue, index)

    End Sub

    ''' <summary>

    ''' During merge, replace the existing (target) value in the entity we are merging to with a new (original) value from the detached entity we are merging.

    ''' </summary>

    ''' <param name="original">the value from the detached entity being merged</param>

    ''' <param name="target">the value in the managed entity</param>

    ''' <param name="owner">the managed entity</param>

    ''' <returns>the value to be merged</returns>

    ''' <remarks>For immutable objects, or null values, it is safe to simply return the first parameter.

    ''' For mutable objects, it is safe to return a copy of the first parameter.

    ''' For objects with component values, it might make sense to recursively replace component values.</remarks>

    Public Function Replace(ByVal original As Object, ByVal target As Object, ByVal owner As Object) As Object Implements NHibernate.UserTypes.IUserType.Replace

        Return original

    End Function

    ''' <summary>

    ''' The type returned by NullSafeGet()

    ''' </summary>

    ''' <value></value>

    ''' <returns></returns>

    ''' <remarks></remarks>

    Public ReadOnly Property ReturnedType() As System.Type Implements NHibernate.UserTypes.IUserType.ReturnedType

        Get

            Return GetType(T)

        End Get

    End Property

    ''' <summary>

    ''' The SQL types for the columns mapped by this type.

    ''' </summary>

    ''' <value></value>

    ''' <returns></returns>

    ''' <remarks></remarks>

    Public MustOverride ReadOnly Property SqlTypes() As NHibernate.SqlTypes.SqlType() Implements NHibernate.UserTypes.IUserType.SqlTypes

End Class

Note that you’ll need to override the SqlTypes() property. This tells NHibernate the type of the database column. So in the following example, as the table column is probably a char(1), we use GetString(1).

Imports NHibernate.SqlTypes

Public Class SexTypeUserType

    Inherits ValueConverterUserType(Of Sex)

    Public Overrides ReadOnly Property SqlTypes() As NHibernate.SqlTypes.SqlType()

        Get

            Dim a() As SqlType = {SqlTypeFactory.GetString(1)}

            Return a

        End Get

    End Property

End Class

Mapping file

Now in the hbm.xml mapping file, you tell NHibernate to use your IUserType class:

    <property name\="Gender" access\="property" type\="MyNameSpace.SexTypeUserType,MyAssemblyName"\>

      <column name\="PERSON\_GENDER" not-null\="true" sql-type\="char(1)" />

    </property\>

ActiveWriter users

If you use ActiveWriter to generate your classes and hbm.xml mapping files, please be aware that it is currently unable to resolve your custom user type. Hopefully this will be resolved in future releases.

The workaround for now is to generate with ActiveWriter, then hand-edit the appropriate hbm and code files. Just remember if you regenerate, you’ll have to reapply your edits!

Windows Live

Thursday, 8 November 2007

There’s a new version of Windows Live Messenger - v8.5.1302.1018 and the RTM of Windows Live Writer.

Get them both from the one installer.

I’m using Live Writer to create most of my blog posts now - it’s a really nice editor, and you can tell it to generate XHTML (go to your weblog settings, look in the Advanced section and click XHTML).

Microsoft Visual Basic Compiler has stopped working

Monday, 5 November 2007

It would be me that happens to make the Visual Basic compiler crash!

As I’ve mentioned in the Rhino Mocks group, the crash appears to be caused by an interesting combination of a generic interface, another interface that inherits from the generic one, and a mock object that implements that interface. When you then try to use the new void method handling code (a new feature of Rhino Mocks 3.3) the compiler barfs (that’s a compiler technical term).

I’ve attached a simple project to the MS bug report that reproduces the crash. Please contact me if you are interested in the code.

The workaround is to revert to the pre-3.3 way of handing void method expectations:

eg. instead of this:

Using Mocks.Record()

Expect.Call(MockedObject.Subroutine)

End Using

Do this:

Using Mocks.Record()

MockedObject.Subroutine

End Using

Fix it Pat (part 2)

Monday, 5 November 2007

Photo of David from Sunday Mail As we arrived at Church on Sunday, I was greeted by a number of people saying, “We saw you in the paper today!”

So while I didn’t get a mention from Thursday’s interview on the day, apparently my mug is in the paper edition, along with along with some of my comments (down under the Belair sub-heading).

To be fair to the Sunday Mail, they are being pretty open about the fact that they were the ones handing out the badges, and I think I’ve been quoted accurately, which is reassuring.

(Photo from Sunday Mail, 4th November 2007)

Fix it Pat

Friday, 2 November 2007

Today at Blackwood Train station, I was interviewed by a Sunday Mail journalist.

Apparently they are running a campaign to encourage more investment in public transport, particularly after the problems the trains had yesterday with the computer system failing completely.

I didn’t make the copy in the final article - oh well.

Owen Vinall, 56, of Coromandel Valley and Susan Lewis, 49, of Blackwood ride the Belair train this morning. Picture: JENNIE GROOM

Looks like I will need to get Owen’s autograph after all (he’s the guy on the left). I was sitting just out of shot behind the guy standing up near the door on the right :-)