Mapping Timestamp Data Using NHibernate’s ICompositeUserType


In my previous post, I took some string data and mapped it directly to a boolean property on an entity. That was pretty simple, but I wanted to try it out on a little more complex object..

In our projects, most of the entities have a Timestamp property which is of type Timestamp:

public class User
{
    public string UserID { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public Timestamp Timestamp { get; set; }
}

and here’s the Timestamp class:

public class Timestamp
{
    public string CreatedByStaff { get; set; }
    public string UpdatedByStaff { get; set; }
    public DateTime? CreatedDateTime { get; set; }
    public DateTime? UpdatedDateTime { get; set; }
}

In the database, the USER table would have the obvious columns for UserID, Username, and Password, but also have the columns for the Timestamp which in my case are CREATED_BY_STAFF, UPDATED_BY_STAFF, CREATED_DATETIME, and UPDATED_DATETIME. Now this can easily be mapped as it is by using a component like so:

<component name="Timestamp" class="XYZ.Core.Timestamp, XYZ.Core">
  <property name="CreatedByStaff" type="String" column="CREATED_BY_STAFF" not-null="true"/>
  <property name="UpdatedByStaff" type="String" column="UPDATED_BY_STAFF" not-null="false"/>
  <property name="CreatedDateTime" type="DateTime" column="CREATED_DATETIME" not-null="true"/>
  <property name="UpdatedDateTime" type="DateTime" column="UPDATED_DATETIME" not-null="false"/>
</component>

That works well, but I hate having to repeat that all over the place. I would rather just have one single line that maps a Timestamp. I experimented with creating a custom mapping type using the ICompositeUserType and got a little closer to the goal. Here’s the class:

public class TimestampMappingType : ICompositeUserType
{
    public bool IsMutable
    {
        get { return true; }
    }

    public Type ReturnedClass
    {
        get { return typeof(Timestamp); }
    }

    public string[] PropertyNames
    {
        get { return new[] { "CreatedByStaff", "UpdatedByStaff", "CreatedDateTime", "UpdatedDateTime" }; }
    }

    public IType[] PropertyTypes
    {
        get { return new[] { NHibernateUtil.String, NHibernateUtil.String, NHibernateUtil.DateTime, NHibernateUtil.DateTime}; }
    }

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

    public object GetPropertyValue(object component, int property)
    {
        var timestamp = AsTimestamp(component);

        switch(property)
        {
            case 0:
                return timestamp.CreatedByStaff;
            case 1:
                return timestamp.UpdatedByStaff;
            case 2:
                return timestamp.CreatedDateTime;
            case 3:
                return timestamp.UpdatedDateTime;
            default:
                throw new YourException("No implementation for property index of '{0}'.", property);
        }
    }

    public void SetPropertyValue(object component, int property, object value)
    {
        if (component == null)
            throw new ArgumentNullException("component");

        var timestamp = AsTimestamp(component);
        switch (property)
        {
            case 0:
                timestamp.CreatedByStaff = (string)value;
                break;
            case 1:
                timestamp.UpdatedByStaff = (string)value;
                break;
            case 2:
                timestamp.CreatedDateTime = (DateTime?)value;
                break;
            case 3:
                timestamp.UpdatedDateTime = (DateTime?)value;
                break;
            default:
                throw new YourException("No implementation for property index of '{0}'.", property);
        }           
    }

    public object NullSafeGet(IDataReader dr, string[] names, ISessionImplementor session, object owner)
    {
        var createdByStaff = NHibernateUtil.String.NullSafeGet(dr, names[0]);
        var updatedByStaff = NHibernateUtil.String.NullSafeGet(dr, names[1]);
        var createdDateTime = NHibernateUtil.DateTime.NullSafeGet(dr, names[2]);
        var updatedDateTime = NHibernateUtil.DateTime.NullSafeGet(dr, names[3]);

        return new Timestamp
        {
            CreatedByStaff = (string)createdByStaff,
            UpdatedByStaff = (string)updatedByStaff,
            CreatedDateTime = (DateTime?)createdDateTime,
            UpdatedDateTime = (DateTime?)updatedDateTime
        };
    }

    public void NullSafeSet(IDbCommand cmd, object value, int index, ISessionImplementor session)
    {
        if (value == null)
        {
            ((IDataParameter)cmd.Parameters[index]).Value = DBNull.Value;
            ((IDataParameter)cmd.Parameters[index+1]).Value = DBNull.Value;
            ((IDataParameter)cmd.Parameters[index+2]).Value = DBNull.Value;
            ((IDataParameter)cmd.Parameters[index+3]).Value = DBNull.Value;
        }
        else
        {
            var timestamp = AsTimestamp(value);

            ((IDataParameter)cmd.Parameters[index]).Value = (object)timestamp.CreatedByStaff ?? DBNull.Value;
            ((IDataParameter)cmd.Parameters[index + 1]).Value = (object)timestamp.UpdatedByStaff ?? DBNull.Value;
            ((IDataParameter)cmd.Parameters[index + 2]).Value = (object)timestamp.CreatedDateTime ?? DBNull.Value;
            ((IDataParameter)cmd.Parameters[index + 3]).Value = (object)timestamp.UpdatedDateTime ?? DBNull.Value;
        }
    }

    public object DeepCopy(object value)
    {
        if(value == null) return null;

        var original = AsTimestamp(value);

        return new Timestamp
                   {
                        CreatedByStaff = original.CreatedByStaff, 
                        UpdatedByStaff = original.UpdatedByStaff,
                        CreatedDateTime = original.CreatedDateTime,
                        UpdatedDateTime = original.UpdatedDateTime
                    };
    }

    public object Disassemble(object value, ISessionImplementor session)
    {
        return DeepCopy(value);
    }

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

    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 == null ? typeof(Timestamp).GetHashCode() + 321 : x.GetHashCode();
    }

    private static Timestamp AsTimestamp(object value)
    {
        if (value == null) return null;

        var ts = value as Timestamp;

        if(ts == null)
            throw new YourException("Expected '{0}' but recieved '{1}'.", typeof(Timestamp), value.GetType());

        return ts;
    }
}

Using this class in my mapping, I can now shorten the Timestamp mapping to:

<property name="Timestamp" type="XYZ.DataAccess.TimestampMappingType, XYZ.DataAccess">
  <column name="CREATED_BY_STAFF" />
  <column name="UPDATED_BY_STAFF" />
  <column name="CREATED_DATETIME" />
  <column name="UPDATED_DATETIME" />
</property>

That’s a little better but I was hoping I could get away with not having to define the columns (they’re the same on every table) but I don’t see a way to set defaults (maybe I’m in the wrong place?). In the end, I’m not sure this really buys me much more than just mapping Timestamp as a component, but I’ll poke around a little more to see if I can figure it out.

Technorati Tags: ,
Mapping Strings to Booleans Using NHibernate’s IUserType