NHibernate doesn’t support mappings for nullable DateTime types by default. Fortunately, this is easy to implement using the IUserType interface. To demonstrate this, create a new Console Application project and add FluentNHibernate using NuGet. Then add the following class:
using System; using System.Data; using NHibernate; using NHibernate.SqlTypes; using NHibernate.UserTypes; namespace UserTypes { /// <summary> /// Type to allow NHibernate to persist DateTime? objects /// </summary> public class NullableDateTimeType : IUserType { /// <summary> /// Gets a value indicating whether the value is mutable /// </summary> public bool IsMutable { get { // This item is immutable: return false; } } /// <summary> /// Gets the type returned by NullSafeGet() /// </summary> public Type ReturnedType { get { return typeof(DateTime?); } } /// <summary> /// Gets the SQL types for the columns mapped by this type. /// </summary> public SqlType[] SqlTypes { get { return new[] { new SqlType(DbType.DateTime) }; } } /// <summary> /// Reconstruct an object from the cacheable representation. At the very least this method should perform a deep copy if the type is mutable. /// </summary> /// <param name="cached">The cached object</param> /// <param name="owner">The owner object</param> /// <returns>The assemled object</returns> public object Assemble(object cached, object owner) { // Used for caching. As our object is immutable we can return as is: return cached; } /// <summary> /// Return a deep copy of the persistent state, stopping at entities and at collections. /// </summary> /// <param name="value">The item to copy</param> /// <returns>The copied item</returns> public object DeepCopy(object value) { // We deep copy the item by creating a new instance with the same contents. // Note that this happens for free with value types because of the way // that method parameters work: if (value == null) { return null; } return value as DateTime?; } /// <summary> /// Transform the object into its cacheable representation. 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. /// </summary> /// <param name="value">The cached object</param> /// <returns>The dassassemled object</returns> public object Disassemble(object value) { // Used for caching. As our object is immutable we can return as is: return value; } /// <summary> /// Compare two instances of the class mapped by this type for persistent "equality" ie. equality of persistent state /// </summary> /// <param name="x">The first item</param> /// <param name="y">The second item</param> /// <returns>A value indicating whether the items are equal</returns> public new bool Equals(object x, object y) { if (x == null && y == null) { return true; } if (x == null) { return false; } return x.Equals(y); } /// <summary> /// Get a hashcode for the instance, consistent with persistence "equality" /// </summary> /// <param name="x">The value to get the hash code for</param> /// <returns>The hash code</returns> public int GetHashCode(object x) { if (x == null) { return 0; } return x.GetHashCode(); } /// <summary> /// Retrieve an instance of the mapped class from a resultset. Implementors should handle possibility of null values. /// </summary> /// <param name="rs">The reader</param> /// <param name="names">The item names</param> /// <param name="owner">The owner object</param> /// <returns>The object requested</returns> public object NullSafeGet(IDataReader rs, string[] names, object owner) { // We get the DateTime from the database using the NullSafeGet used to get strings from NHibernateUtil: return NHibernateUtil.DateTime.NullSafeGet(rs, names[0]) as DateTime?; } /// <summary> /// Write an instance of the mapped class to a prepared statement. Implementors should handle possibility of null values. A multi-column type should be written to parameters starting from index. /// </summary> /// <param name="cmd">The command</param> /// <param name="value">The value to use</param> /// <param name="index">The index to set</param> public void NullSafeSet(IDbCommand cmd, object value, int index) { // Convert to the correct type and set: var dateTimeValue = value as DateTime?; if (dateTimeValue == null) { NHibernateUtil.DateTime.NullSafeSet(cmd, null, index); } else { NHibernateUtil.DateTime.NullSafeSet(cmd, dateTimeValue.Value, index); } } /// <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. 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. /// </summary> /// <param name="original">The original value</param> /// <param name="target">The target value</param> /// <param name="owner">The owner object</param> /// <returns>The replacement object</returns> public object Replace(object original, object target, object owner) { // As our object is immutable we can just return the original return original; } } }
This class is responsible for telling NHibernate how to save and retrieve nullable DateTime values. The following demonstrates how this class can be used.
Given a class with a nullable DateTime property:
using System; namespace UserTypes { public class Student { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime? GraduationDate { get; set; } } }
…the mapping would be as follows:
using FluentNHibernate.Mapping; using UserTypes; public class StudentMapping : ClassMap<Student> { public StudentMapping() { Not.LazyLoad(); Id(x => x.Id).GeneratedBy.Identity(); Map(x => x.FirstName).Length(20).Not.Nullable(); Map(x => x.LastName).Length(20).Not.Nullable(); Map(x => x.GraduationDate).Nullable().CustomType(x => typeof(NullableDateTimeType)); } }
This can be tested by adding the following code to the Program class:
using System; using FluentNHibernate.Cfg; using FluentNHibernate.Cfg.Db; using NHibernate.Cfg; using NHibernate.Tool.hbm2ddl; namespace UserTypes { public static class Program { public static void Main() { var connectionString = "Data Source=(local);Initial Catalog=StudentTest;Integrated Security=True;Pooling=False"; var sessionFactory = Fluently.Configure() .Database(MsSqlConfiguration.MsSql2008.ConnectionString(connectionString).ShowSql()) .ExposeConfiguration(BuildSchema) .Mappings(x => x.FluentMappings.AddFromAssemblyOf<Student>()) .BuildSessionFactory(); // Create the session: using (var session = sessionFactory.OpenSession()) { session.SaveOrUpdate(new Student { FirstName = "Not", LastName = "Graduated" }); session.SaveOrUpdate(new Student { FirstName = "Already", LastName = "Graduated", GraduationDate = new DateTime(2000, 1, 1) }); } using (var session = sessionFactory.OpenSession()) { var students = session.QueryOver<Student>().List(); foreach (var student in students) { Console.WriteLine("{0} {1} {2}", student.FirstName, student.LastName, student.GraduationDate); } } Console.ReadLine(); } private static void BuildSchema(Configuration configuration) { new SchemaExport(configuration).Create(false, true); } } }
Note that the code above assumes there is a local instance of SQL Server running, with a database called StudentTest accessible using integrated authentication.
Running the code creates the following table, which includes a DateTime field that accepts null values as required:
…inserts the following data:
…and outputs the following:
This proves that the mapping can be used to store and retrieve null and non-null DateTime values as required.