Archive

Archive for the ‘NHibernate’ Category

Simplify IUserType Testing

October 16, 2009 Neal 4 comments

Background

The application I am currently working on makes ( will make ) heavy use of the .NET TimeZoneInfo class to allow us to display times in the current users local time. To achieve this we need to capture and store the timezone from the user.

Capturing the timezone is relatively simple, I use a slightly modified version of this technique used by Nate Kohari’s most excellent Zen.

Storing the timezone provides an interesting choice of options;

I could store the raw offset in hours ( a double ) or in minutes ( an int ) and recreate the TimeZoneInfo each time I want to display a date or time in the users local time. This has the benefit of being incredibly simple and, with a little work to create an extension method, generates almost no duplication except the act of converting the double or int into a TimeZoneInfo.

The second alternative was to let my trusty ORM do the work for me. I use NHibernate and after a little research I found the following articles about creating an IUserType that would allow me to map a TimeZoneInfo into a database field. If you want all the gory details of how to do it go and read those articles, otherwise the code for my TimeZoneInfoUserType is shown at the bottom of the post ( see the section titled The Implementation ).

The Useful Stuff

Normally I use a BDD style approach for building something like this, but as I wasn’t sure how to solve the problem, I spiked out the approach, then added the tests and refined the implementation as required.

Part of spiking out the solution was figuring out how to test it and this initially tripped me up as I was considering mocking IDbCommand and IDataReader ( which gets ugly fast ).

After a little bit of thought, I figured I could use a SqlCommand for the IDbCommand. The following snippet shows what you need to do to use a SqlCommand for testing your NullSafeSet ( note where parameter index comes from ).

_command = new SqlCommand();
_parameterIndex = _command.Parameters.Add( _command.CreateParameter() );

// ... this is then used in the NullSafeSet test ...
_userType.NullSafeSet( _command, null, _parameterIndex );

To find an acceptable IDataReader stand-in, I went poking around with Reflector and found an IDataReader implementation that wraps a DataTable. This gave me a means of quickly and easily controlling the data being passed to the code under test. The following snippet shows how you can set up an IDataReader from a DataTable in as little as 5 lines of code for testing your NullSafeGet ( note the data reader must be “initialised” by calling Read() before using it ):

var dataTable = new DataTable();
dataTable.Columns.Add( ColumnName, typeof( int ) );
dataTable.LoadDataRow( new object[] {TimezoneOffset * 60}, true );
_dataReader = new DataTableReader( dataTable );
_dataReader.Read();

// ... this is then used in the NullSafeSet test ...
_timezoneInfo = (TimeZoneInfo) _userType.NullSafeGet( _dataReader, new[] {ColumnName}, null );

The full source for the TimeZoneInfoUserType and the associated tests is shown below:

The Implementation

    public class TimezoneInfoUserType : IUserType
    {
        public new bool Equals(object x, object y)
        {
            if( ReferenceEquals( x, y ) )
            {
                return true;
            }
            if( x == null || y == null )
            {
                return false;
            }

            return x.Equals( y );
        }

        public int GetHashCode( object x )
        {
            return x.GetHashCode();
        }

        public object NullSafeGet( IDataReader rs, string[] names, object owner )
        {
            var rawValue = NHibernateUtil.Int32.NullSafeGet( rs, names[ 0 ] );
            if( rawValue == null )
            {
                return null;
            }
            var timezoneOffsetMinutes = (int) rawValue;
            var timezoneOffset = new TimeSpan( 0, timezoneOffsetMinutes, 0 );
            return TimeZoneInfoFactory.Create( timezoneOffset.TotalHours );
        }

        public void NullSafeSet( IDbCommand cmd, object value, int index )
        {
            if(value == null)
            {
                NHibernateUtil.Int32.NullSafeSet( cmd, null, index );
                return;
            }
            var timezoneInfo = (TimeZoneInfo) value;
            var offsetInMinutes = timezoneInfo.BaseUtcOffset.TotalMinutes;
            NHibernateUtil.Int32.NullSafeSet( cmd, Convert.ToInt32( offsetInMinutes ) , index );
        }

        public object DeepCopy( object value )
        {
            return value;
        }

        public object Replace( object original, object target, object owner )
        {
            return original;
        }

        public object Assemble( object cached, object owner )
        {
            return cached;
        }

        public object Disassemble( object value )
        {
            return value;
        }

        public SqlType[] SqlTypes
        {
            get { return new[] {SqlTypeFactory.Int32}; }
        }

        public Type ReturnedType
        {
            get { return typeof( TimeZoneInfo ); }
        }

        public bool IsMutable
        {
            get { return false; }
        }
    }

The Tests
Note: If you are wondering about the =()=> sequence, I use MSpec for my testing.

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_saving_a_null_TimezoneInfo
    {
        static int _parameterIndex;
        static TimezoneInfoUserType _userType;
        static IDbCommand _command;

        Establish context = () =>
        {
            _userType = new TimezoneInfoUserType();
            _command = new SqlCommand();
            _parameterIndex = _command.Parameters.Add( _command.CreateParameter() );
        };

        Because of = () =>
            _userType.NullSafeSet( _command, null, _parameterIndex );

        It the_offset_is_stored_as_null = () =>
            ((IDataParameter)_command.Parameters[ _parameterIndex ]).Value.ShouldEqual(DBNull.Value);
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_saving_a_TimezoneInfo_with_a_zero_offset
    {
        static int _parameterIndex;
        static TimezoneInfoUserType _userType;
        static IDbCommand _command;
        static TimeZoneInfo _timezoneInfo;

        Establish context = () =>
        {
            _timezoneInfo = TimeZoneInfoFactory.Create( 0 );
            _userType = new TimezoneInfoUserType();
            _command = new SqlCommand();
            _parameterIndex = _command.Parameters.Add( _command.CreateParameter() );
        };

        Because of = () =>
            _userType.NullSafeSet(_command, _timezoneInfo, _parameterIndex);

        It the_offset_is_stored_as_zero = () =>
            ( (IDataParameter) _command.Parameters[ _parameterIndex ] ).Value.ShouldEqual( 0 );
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_saving_a_TimezoneInfo_with_a_positive_offset
    {
        const int TimezoneOffset = 12;
        static int _parameterIndex;
        static TimezoneInfoUserType _userType;
        static IDbCommand _command;
        static TimeZoneInfo _timezoneInfo;

        Establish context = () =>
        {
            _timezoneInfo = TimeZoneInfoFactory.Create( TimezoneOffset );
            _userType = new TimezoneInfoUserType();
            _command = new SqlCommand();
            _parameterIndex = _command.Parameters.Add( _command.CreateParameter() );
        };

        Because of = () =>
            _userType.NullSafeSet( _command, _timezoneInfo, _parameterIndex );

        It the_offset_is_stored_in_minutes = () =>
            ( (IDataParameter) _command.Parameters[ _parameterIndex ] ).Value.ShouldEqual( TimezoneOffset * 60 );
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_saving_a_TimezoneInfo_with_a_negative_offset
    {
        const int TimezoneOffset = -12;
        static int _parameterIndex;
        static TimezoneInfoUserType _userType;
        static IDbCommand _command;
        static TimeZoneInfo _timezoneInfo;

        Establish context = () =>
        {
            _timezoneInfo = TimeZoneInfoFactory.Create( TimezoneOffset );
            _userType = new TimezoneInfoUserType();
            _command = new SqlCommand();
            _parameterIndex = _command.Parameters.Add( _command.CreateParameter() );
        };

        Because of = () =>
            _userType.NullSafeSet( _command, _timezoneInfo, _parameterIndex );

        It the_offset_is_stored_in_minutes = () =>
            ( (IDataParameter) _command.Parameters[ _parameterIndex ] ).Value.ShouldEqual( TimezoneOffset * 60 );
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_retrieving_a_null_TimezoneInfo
    {
        const string ColumnName = "Timezone";
        static TimeZoneInfo _timezoneInfo;
        static TimezoneInfoUserType _userType;
        static IDataReader _dataReader;

        Establish context = () =>
        {
            _userType = new TimezoneInfoUserType();
            var dataTable = new DataTable();
            dataTable.Columns.Add( ColumnName, typeof( int ) );
            dataTable.LoadDataRow( new object[] {null}, true );
            _dataReader = new DataTableReader( dataTable );
            _dataReader.Read();
        };

        Because of = () =>
            _timezoneInfo = (TimeZoneInfo) _userType.NullSafeGet( _dataReader, new[] {ColumnName}, null );

        It returns_null = () =>
            _timezoneInfo.ShouldBeNull();
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_retrieving_a_TimezoneInfo_with_a_zero_offset
    {
        const string ColumnName = "Timezone";
        static TimeZoneInfo _timezoneInfo;
        static TimezoneInfoUserType _userType;
        static IDataReader _dataReader;

        Establish context = () =>
        {
            _userType = new TimezoneInfoUserType();
            var dataTable = new DataTable();
            dataTable.Columns.Add( ColumnName, typeof( int ) );
            dataTable.LoadDataRow( new object[] {0}, true );
            _dataReader = new DataTableReader( dataTable );
            _dataReader.Read();
        };

        Because of = () =>
            _timezoneInfo = (TimeZoneInfo) _userType.NullSafeGet( _dataReader, new[] {ColumnName}, null );

        It the_BaseUtcOffset_is_zero = () =>
            _timezoneInfo.BaseUtcOffset.TotalHours.ShouldEqual( 0d );

        It the_displayname_is_GMT = () =>
            _timezoneInfo.DisplayName.ShouldEqual( "GMT" );
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_retrieving_a_TimezoneInfo_with_a_positive_offset
    {
        const string ColumnName = "Timezone";
        const int TimezoneOffset = 12;
        static TimeZoneInfo _timezoneInfo;
        static TimezoneInfoUserType _userType;
        static IDataReader _dataReader;

        Establish context = () =>
        {
        _userType = new TimezoneInfoUserType();
        var dataTable = new DataTable();
        dataTable.Columns.Add(ColumnName, typeof(int));
        dataTable.LoadDataRow(new object[] { TimezoneOffset * 60 }, true);
        _dataReader = new DataTableReader(dataTable);
        _dataReader.Read();
        };

        Because of = () =>
            _timezoneInfo = (TimeZoneInfo)_userType.NullSafeGet(_dataReader, new[] { ColumnName }, null);

        It the_BaseUtcOffset_is_generated_from_the_number_of_minutes_retrieved = () =>
            _timezoneInfo.BaseUtcOffset.TotalHours.ShouldEqual( TimezoneOffset );

        It the_displayname_follows_the_pattern_GMT_plus_timezone_offset_ = () =>
            _timezoneInfo.DisplayName.ShouldEqual( "GMT+" + TimezoneOffset );
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_retrieving_a_TimezoneInfo_with_a_negative_offset
    {
        const string ColumnName = "Timezone";
        const int TimezoneOffset = -12;
        static TimeZoneInfo _timezoneInfo;
        static TimezoneInfoUserType _userType;
        static IDataReader _dataReader;

        Establish context = () =>
        {
            _userType = new TimezoneInfoUserType();
            var dataTable = new DataTable();
            dataTable.Columns.Add( ColumnName, typeof( int ) );
            dataTable.LoadDataRow( new object[] {TimezoneOffset * 60}, true );
            _dataReader = new DataTableReader( dataTable );
            _dataReader.Read();
        };

        Because of = () =>
            _timezoneInfo = (TimeZoneInfo) _userType.NullSafeGet( _dataReader, new[] {ColumnName}, null );

        It the_BaseUtcOffset_is_generated_from_the_number_of_minutes_retrieved = () =>
            _timezoneInfo.BaseUtcOffset.TotalHours.ShouldEqual( TimezoneOffset );

        It the_displayname_follows_the_pattern_GMT_minus_timezone_offset_ = () =>
            _timezoneInfo.DisplayName.ShouldEqual( "GMT" + TimezoneOffset );
    }
Categories: NHibernate, TDD, Unit Testing

NHibernate Lambda Extentions Gotcha

March 4, 2009 Neal Leave a comment

Recently I came across the open source NHibernate Lambda Extensions project on Google Code.  This small library provides a set of extension to remove the magic strings from your NHibernate criteria queries, and it’s great (once you get used to the conventions).

I’ve been slowly migrating queries across to use NHLambda and tripped up on the following query which is effectively Survey.Status == Complete and Survey.Questionnaire.Id = questionnaireId.


            var expected = DetachedCriteria.For<Survey>()
                .Add( Restrictions.Eq( "Status", SurveyStatus.Complete ) )
                .Add( Restrictions.Eq( "Questionnaire.Id", questionnaireId ) )
                .CreateAlias( "Answers", "answerAlias" )
                .SetProjection( Projections.Property( "answerAlias.Id" ) );

Initially I had converted it as


            Answer answerAlias = null;
            var actual = DetachedCriteria.For<Survey>()
                .Add<Survey>( s => s.Status == SurveyStatus.Complete )
                .Add<Questionnaire>( q => q.Id == questionnaireId )
                .CreateAlias<Survey>( s => s.Answers, () => answerAlias )
                .SetProjection( LambdaProjection.Property( () => answerAlias.Id ) );

which actually generates a query equivalent to Survey.Status == Complete and Survey.Id == questionnaireId.

After kicking myself for being lazy and not writing a test first, I wrote a test and realised that I had implicitly traversed an association in the second restriction.  There are two ways to solve this, either explicitly create the alias, or (as I have just considered and tested while writing this post) correctly place the restriction on the survey and navigate to Questionnaire.Id.  Both approaches are shown below.


            // Explicitly create and navigate the alias
            Questionnaire questionnaireAlias = null;
            Answer answerAlias = null;
            var actual = DetachedCriteria.For<Survey>()
                .Add<Survey>( s => s.Status == SurveyStatus.Complete )
                .CreateAlias<Survey>( s => s.Questionnaire, () => questionnaireAlias )
                .Add( () => questionnaireAlias.Id == questionnaireId )
                .CreateAlias<Survey>( s => s.Answers, () => answerAlias )
                .SetProjection( LambdaProjection.Property( () => answerAlias.Id ) );

            // Apply the restriction to Survey and navigate to Questionnaire.Id
            var actual2 = DetachedCriteria.For<Survey>()
                .Add<Survey>(s => s.Status == SurveyStatus.Complete)
                .Add<Survey>(s => s.Questionnaire.Id == questionnaireId)
                .CreateAlias<Survey>(s => s.Answers, () => answerAlias)
                .SetProjection(LambdaProjection.Property(() => answerAlias.Id));

I don’t know which one of these is more correct (or even if one is better that the other), but they both look a hell of a lot better than the original with it’s many magic strings.

Anyway, the moral of this story is twofold; one, beware implicit association traversals when migrating queries to NHLambda and two, don’t be a clown – write unit tests to cover the migrated queries!  =)

NHibernate many-to-many mappings and order-by

February 3, 2009 Neal 1 comment

Just found this and it took me a while to figure out so posting here to remind myself as well as help anyone else who stumbles over the issue.

When you configure collection mappings in NHibernate, you can add an order-by attribute and specify the field to order the collection by as well as whether to sort ascending or descending.

With one-to-many mappings, the field you specify is a member of the entity contained in the collection so the following would contain a collection of Question instances sorted by DisplayIndex.

<bag name="Questions" cascade="all" order-by="DisplayIndex asc" lazy="false" fetch ="subselect">
   <key column="QuestionnaireSection" />
      <one-to-many class="Question" />
    </bag>

However, if the mapping is a many-to-many, the order by clause is applied to the association table – the table in the middle of a many-to-many association mapped into a relational database. I cannot use the order-by attribute on the following collection mapping.

    <bag name="SelectedOptions" cascade="none" lazy="false">
      <key column="Answer" />
      <many-to-many column="AnswerOption" class="AnswerOption" />
    </bag>

The implementation of this may be erroneous (and could be argued as counter-intuitive), however as it stands the order-by clause of a collection mapped as a many-to-many is not overly useful as we cannot sort a collection of entities in the same manner as we can with a many-to-one mapping.

Categories: NHibernate

Restricting types returned from a polymorphic query

September 19, 2008 Neal Leave a comment

NHibernate is cool, I can map an inheritance hierarchy into my database, ask it to get me all instances of an object at the root of the hierarchy and NHibernate will automatically retrieve all subclasses and instantiate the actual (derived) type.  What does this mean in practice?

Demo class hierarchy

Demo class hierarchy

If I have the following class hierarchy and I query

“from Animal”

I will get a list of Animal instances back, but looking at the type of each returned instance will show that they are either Cocker Spaniel or West Highland Terrier.

For the most part, this behaviour is exactly what you need to leverage polymorphism in your design, however on the odd occasion, you need to limit the returned type from a polymorphic query (in my case it was to build a report). To achieve this you need to leverage the class meta-property and change your query to

“from Animal pet where pet.class = Cocker Spaniel”

The key to remember when using class is that you must reference it from the alias you assign to the type you want to restrict (e.g. pet in this example).

Categories: NHibernate