NHibernate Pitfalls: Custom Types and Detecting Changes

This is part of a series of posts about NHibernate Pitfalls. See the entire collection here.

NHibernate supports the declaration of properties of user-defined types, that is, not entities, collections or primitive types. These are used for mapping a database columns, of any type, into a different type, which may not even be an entity; think, for example, of a custom user type that converts a BLOB column into an Image.

User types must implement interface NHibernate.UserTypes.IUserType. This interface specifies an Equals method that is used for comparing two instances of the user type. If this method returns false, the entity is marked as dirty, and, when the session is flushed, will trigger an UPDATE. So, in your custom user type, you must implement this carefully so that it is not mistakenly considered changed. For example, you can cache the original column value inside of it, and compare it with the one in the other instance.

Let’s see an example implementation of a custom user type that converts a Byte[] from a BLOB column into an Image:

   1: [Serializable]
   2: public sealed class ImageUserType : IUserType
   3: {
   4:     private Byte[] data = null;
   5:  
   6:     public ImageUserType()
   7:     {
   8:         this.ImageFormat = ImageFormat.Png;
   9:     }
  10:  
  11:     public ImageFormat ImageFormat
  12:     {
  13:         get;
  14:         set;
  15:     }
  16:  
  17:     public Boolean IsMutable
  18:     {
  19:         get
  20:         {
  21:             return (true);
  22:         }
  23:     }
  24:  
  25:     public Object Assemble(Object cached, Object owner)
  26:     {
  27:         return (cached);
  28:     }
  29:  
  30:     public Object DeepCopy(Object value)
  31:     {
  32:         return (value);
  33:     }
  34:  
  35:     public Object Disassemble(Object value)
  36:     {
  37:         return (value);
  38:     }
  39:  
  40:     public new Boolean Equals(Object x, Object y)
  41:     {
  42:         return (Object.Equals(x, y));
  43:     }
  44:  
  45:     public Int32 GetHashCode(Object x)
  46:     {
  47:         return ((x != null) ? x.GetHashCode() : 0);
  48:     }
  49:  
  50:     public override Int32 GetHashCode()
  51:     {
  52:         return ((this.data != null) ? this.data.GetHashCode() : 0);
  53:     }
  54:  
  55:     public override Boolean Equals(Object obj)
  56:     {
  57:         ImageUserType other = obj as ImageUserType;
  58:  
  59:         if (other == null)
  60:         {
  61:             return (false);
  62:         }
  63:  
  64:         if (Object.ReferenceEquals(this, other) == true)
  65:         {
  66:             return (true);
  67:         }
  68:  
  69:         return (this.data.SequenceEqual(other.data));
  70:     }
  71:  
  72:     public Object NullSafeGet(IDataReader rs, String[] names, Object owner)
  73:     {
  74:         Int32 index = rs.GetOrdinal(names[0]);
  75:         Byte[] data = rs.GetValue(index) as Byte[];
  76:  
  77:         this.data = data as Byte[];
  78:  
  79:         if (data == null)
  80:         {
  81:             return (null);
  82:         }
  83:  
  84:         using (MemoryStream stream = new MemoryStream(this.data ?? new Byte[0]))
  85:         {
  86:             return (Image.FromStream(stream));
  87:         }
  88:     }
  89:  
  90:     public void NullSafeSet(IDbCommand cmd, Object value, Int32 index)
  91:     {
  92:         if (value != null)
  93:         {
  94:             Image data = value as Image;
  95:  
  96:             using (MemoryStream stream = new MemoryStream())
  97:             {
  98:                 data.Save(stream, this.ImageFormat);
  99:                 value = stream.ToArray();
 100:             }
 101:         }
 102:  
 103:         (cmd.Parameters[index] as DbParameter).Value = value ?? DBNull.Value;
 104:     }
 105:  
 106:     public Object Replace(Object original, Object target, Object owner)
 107:     {
 108:         return (original);
 109:     }
 110:  
 111:     public Type ReturnedType
 112:     {
 113:         get
 114:         {
 115:             return (typeof(Image));
 116:         }
 117:     }
 118:  
 119:     public SqlType[] SqlTypes
 120:     {
 121:         get
 122:         {
 123:             return (new SqlType[] { new SqlType(DbType.Binary) });
 124:         }
 125:     }
 126: }

In this case, we need to cache the original Byte[] data because it’s not easy to compare two Image instances, unless, of course, they are the same.

                             

No Comments