PrimeDigit – A Design Blog by Will Shaver

December 2, 2008

Sql Server DateTime Conversion

Filed under: NHibernate,c# — Will @ 10:05 am

Suppose that your model requires selecting over a date range. It is frequently more convenient to store DateTime.MinValue than it is to store a NULL as this allows for easy selecting without doing a bunch of field < value or null or field > value or not null monkey business. The only problem is that DateTime.MinValue doesn’t fit into SQL server, and DateTime.MaxValue won’t store correctly either. You can even end up where you get a situation where you are storing DateTime.MaxValue then checking a freshly loaded entity to see if it equals DateTime.MaxValue and it doesn’t!

One solution is to create an extension method to trim down the DateTime to only include valid values in a deterministic way as follows:

    public static class DateTimeExtensions
    {
        private static readonly DateTime minSqlDateTime = DateTime.Parse("1/1/1753 12:00:00 AM");
 
        public static DateTime ToSqlDateTime(this DateTime dt)
        {
            if (dt < minSqlDateTime)
                dt = minSqlDateTime;
            return new DateTime(dt.Ticks - (dt.Ticks % 10000000));            
        }
    }

Note that this is designed for SQL Server. Other database types may need different levels of precision.

Instead you could implement a custom IUserType for NHibernate which tells NHibernate how to translate a DateTime that is out of bounds into one that is in bounds such as:

    public class SqlDateTimeUserType : IEnhancedUserType
    {
        private static readonly DateTime minSqlDateTime = DateTime.Parse("1/1/1753 12:00:00 AM");
 
        public new bool Equals(object x, object y)
        {
            return x == null ? y == null : x.Equals(y);
        }
 
        public Type ReturnedType
        {
            get { return typeof(DateTime); }
        }
 
        public SqlType[] SqlTypes
        {
            get { return new[] { NHibernateUtil.DateTime.SqlType }; }
        }
 
        public int GetHashCode(object x)
        {
            return x.GetHashCode();
        }
 
        public object NullSafeGet(IDataReader dr, string[] names, object owner)
        {
            //no need to alter values coming out of the database
            DateTime? obj = NHibernateUtil.DateTime.NullSafeGet(dr, names[0]) as DateTime?;
            return obj == null ? ToSqlDateTime(DateTime.MinValue) : obj.Value;
        }
 
        public void NullSafeSet(IDbCommand cmd, object obj, int index)
        {
            //set it to a safe value going into the database
            ((IDataParameter)cmd.Parameters[index]).Value = ToSqlDateTime((DateTime)obj);
        }
 
        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 bool IsMutable
        {
            get { return false; }
        }
 
        public object FromXMLString(string xml)
        {
            return DateTime.Parse(xml);
        }
 
        public string ObjectToSQLString(object value)
        {
            return value as string;
        }
 
        public string ToXMLString(object value)
        {
            return value as string;
        }
 
        private static DateTime ToSqlDateTime(DateTime dt)
        {
            if (dt < minSqlDateTime)
                dt = minSqlDateTime;
            return new DateTime(dt.Ticks - (dt.Ticks % 10000000));
        }
    }

These user types are mapped as follows:

    <property name="ImportDate" type="Framework.SqlDateTimeUserType, Framework"/>

The only problem with this is that a simple test such as the following will fail:

        [Test]
        public void CanSaveBadValuesToImportDate()
        {
            With.AutoRollbackTransaction(() =>
            {
                Product p = new Product("Apples");
                p.ImportDate = DateTime.MinValue;
                Session.Save(p);
                Session.Flush();
                Session.Clear();
                Product p2 = Session.Load<Product>(p.Id);
                Assert.AreEqual(p.Name,p2.Name);
                Assert.AreEqual(p.ImportDate, p2.ImportDate); //fails here
            });
        }

We could try and create a bunch of edge conditions on our converter such as:

        public object NullSafeGet(IDataReader dr, string[] names, object owner)
        {
            DateTime? obj = NHibernateUtil.DateTime.NullSafeGet(dr, names[0]) as DateTime?;
            if (obj != null && obj == minSqlDateTime)
                return DateTime.MinValue;
            return obj == null ? ToSqlDateTime(DateTime.MinValue) : obj.Value;
        }

But that could get out of hand in a hurry. It also would cause errors if you stored DateTime.MinValue.AddSeconds(1). If we wanted to go really over the top, we could create our own class that implements operator DateTime (explicit and implicit) which would do the conversion to a SQL range when we first assign a value. This would solve both problems, but you’d end up with your domain needing a special DateTime class for all of your entities instead of using the built in. Your call…

August 26, 2008

ISession Extension for Saving Multiple Objects

Filed under: NHibernate,c# — Will @ 3:36 pm

Incredibly simple, but quite useful:

        public static void Save(this ISession session, params object [] entities)
        {
            foreach (var o in entities)
            {
                session.Save(o);
            }
        }

You can use it like:

session.Save(a,b,c,d,….);

with a list of objects of any type. Useful for unit testing where you want to save multiple items at once.

[Update]

Probably should call it something other than Save, such as SaveAll or SaveList because if you only pass two items to the overload it calls the standard Save(object, int id) version. :(

June 16, 2008

CriteriaByLongAlias

Filed under: NHibernate,c# — Will @ 9:01 am

This simple function cleans up a lot of the Alias sillyness I have fought with in NHibernate.

        public static ICriteria CriteriaByLongAlias(this ICriteria criteria, string field)

        {

            ICriteria byAlias = criteria.GetCriteriaByAlias(criteria.Alias + “_” + field) ??

                                criteria.CreateCriteria(field, criteria.Alias + “_” + field);

            return byAlias;

        }

It is used any time you would previously call CreateCriteria and pass an alias.
Before:
session.CreateCriteria(typeof (Product)).CreateCriteria(“Orders”,”Product_Orders”);
After:
session.CreateCriteria(typeof(Product)).CriteriaByLongAlias(“Orders”);

The alias part is done automatically based on the criteria path. The advantage is that it works recursively, allowing for multiple criteria to be created, and then referenced without duplicates.

ICriteria criteria = session.CreateCriteria(typeof(Product)).CriteriaByLongAlias(“Orders”).CriteriaByLongAlias(“Customer”).Add(Expression.Eq(“Name”, “Ace Hardware”));
criteria.CriteriaByLongAlias(“Orders”).Add(Expression.Eq(“OrderNumber”,12354));

etc.

Note that in order for it to work, you have to create all your subcriteria with it.

-Will

Powered by WordPress