NHibernate/SqlServer: persist a generic ‘serializable’ object to an XML field

Print Content | More

Recently we had the problem of persisting some sort of extended data attached to a business object with the schema and the data of these extended information that can vary over time (usually with an additive strategy). Since we do not need to do extensive or complex analysis queries on these data (they are almost all there for a ‘cosmetic’ fashion) the first thing that came into my mind instead of creating another mapping to a liked table was to use an XML field.

Plus the data that can be stored in this data extension field can be totally different from object to object.

If you know the Type of the object that represent the data to save my good friend Alkamfer already have an elegant solution: Use Xml field in SqlServer with nhibernate.

The first step is to find an efficient way to serialize/deserialize objects to our XML format, I made some tests on the standards .net serializers and it came out that the new DataContractSerializer is quite fast compare to the old XmlSerializer, so I decided to give it a try.

Serializing an object is not a real problem, the deserialization step is a little more problematic, mainly because we need to know the type of the object we want to deserialize. but since we want to deserialize some data coming out from a single XML field in a database we do not have this information available...untill we find a way to embed it inside the XML serialized stream.

So the first step was to write a couple of simple functions to serialize a class to an XML stream embedding the type of the serialized item in the outmost tag. This is quite simple to do using the DataContractSerializer functions: WriteStartObject(), WriteObjectContent() and WriteEndObject().

Here’s the code for the serialization helpers, the Type attribute is embedded in a custom attribute called serType:

public static class DataContractSerializerHelpers
   {
      public static string ToXml(object obj)
      {
         return ToXml(obj, null);
      }
 
      public static string ToXml(object obj, IEnumerable<Type> knownTypes)
      {
         Type objType = obj.GetType();
         DataContractSerializer ser = new DataContractSerializer(objType, knownTypes);
         {
            using (StringWriter sw = new StringWriter())
            {
               XmlWriterSettings settings = new XmlWriterSettings();
               settings.OmitXmlDeclaration = true;
               settings.Indent = true;
               //settings.NewLineOnAttributes = true;
               using (XmlWriter writer = XmlWriter.Create(sw, settings))
               {
                  ser.WriteStartObject(writer, obj);
                  writer.WriteAttributeString("serType", objType.FullName + ", " + objType.Assembly.GetName().Name);
                  ser.WriteObjectContent(writer, obj);
                  ser.WriteEndObject(writer);
                  //ser.WriteObject(writer, obj);
               }
               return sw.ToString();
            }
         }
      }
 
      public static object FromXml(string data)
      {
         return FromXml(data, null);
      }
 
      public static object FromXml(string data, IEnumerable<Type> knownTypes)
      {
         using (StringReader sr = new StringReader(data))
         {
            XmlReaderSettings settings = new XmlReaderSettings();
            using (XmlReader reader = XmlReader.Create(sr, settings))
            {
               reader.Read();
               string objTypeStr = reader.GetAttribute("serType");
               if (string.IsNullOrEmpty(objTypeStr))
               {
                  throw new SerializationException("Missing the 'type' argument to enable automatich deserialization");
               }
               DataContractSerializer ser = new DataContractSerializer(Type.GetType(objTypeStr), knownTypes);
               return ser.ReadObject(reader);
            }
         }
      }
   }

The last step is now to save and load data using NHibernate, the obvious choice is to use a custom UserType; the serialization and deserialization is done in the NullSafeSet() and NullSafeGet() routines, we also choose to implement the IParameterizedType interface to provide for the KnownTypes that the DataContractSerializer may need in order to deserialize the data in the correct way (a simpler solution is to decorate the classes with the KnownTypeAttribute).

Here’s the code for the new user type:

public class XmlFieldUserType : IUserType, IParameterizedType
{
   #region Equals member
 
   bool IUserType.Equals(object x, object y)
   {
      return (x == y) || ((x != null) && x.Equals(y));
   }
 
   #endregion
 
   #region IUserType Members
 
   public object Assemble(object cached, object owner)
   {
      return cached;
   }
 
   public object DeepCopy(object value)
   {
      if (value == null) return null;
 
      return value.DeepClone();
   }
 
   public object Disassemble(object value)
   {
      return value;
   }
 
   public int GetHashCode(object x)
   {
      return x.GetHashCode();
   }
 
   public bool IsMutable
   {
      get { return true; }
   }
 
   public object NullSafeGet(System.Data.IDataReader rs, string[] names, object owner)
   {
      Int32 index = rs.GetOrdinal(names[0]);
      if (rs.IsDBNull(index))
      {
         return null;
      }
 
      return DataContractSerializerHelpers.FromXml((String)rs[index], KnowsTypes);
   }
 
   public void NullSafeSet(System.Data.IDbCommand cmd, object value, int index)
   {
      if (value == null || value == DBNull.Value)
      {
         NHibernateUtil.String.NullSafeSet(cmd, null, index);
      }
      else
      {
         NHibernateUtil.String.Set(cmd, DataContractSerializerHelpers.ToXml(value, KnowsTypes), index);
      }
   }
 
   public object Replace(object original, object target, object owner)
   {
      return original;
   }
 
   public Type ReturnedType
   {
      get { return typeof(Uri); }
   }
 
   public SqlType[] SqlTypes
   {
      get { return new SqlType[] { NHibernateUtil.String.SqlType }; }
   }
 
   #endregion
 
   #region IParameterizedType Members
 
   private string KnownTypesStr;
   private List<Type> KnowsTypes;
 
   public void SetParameterValues(System.Collections.IDictionary parameters)
   {
      KnownTypesStr = (string)parameters["knowntypes"];
 
      if (string.IsNullOrEmpty(KnownTypesStr)) return;
 
      KnowsTypes = new List<Type>();
      foreach(string str in KnownTypesStr.Split(';'))
      {
         Type t = Type.GetType(str, false);
         if (t != null)
            KnowsTypes.Add(t);
      }
   }
 
   #endregion
}

As a note: the DeepClone is actually implemented with a technique already explained in this blog, to be honest it’s not the best way to do it, but it’s a general approach that works well for serializable objects.

To use the previous code, suppose you have the following class hierarchy:

   1: public class Data
   2:   {
   3:      public Guid Id { get; set; }
   4:      public string Data1 { get; set; }
   5:      // contiene diverse strutture, mappato su campo xml
   6:      public object ExtData { get; set; }
   7:   }
   8:  
   9: [Serializable]
  10: public class ExtData
  11: {
  12:    public string ExtData1 { get; set; }
  13:    public string ExtData2 { get; set; }
  14: }
  15:  
  16: [Serializable]
  17: public class ExtData2
  18: {
  19:    public string ExtData3 { get; set; }
  20:    public string ExtData4 { get; set; }
  21: }

with ExtData that can contain instances of data types ExtData and ExtData2 (totally unrelated to each others), you can map the Data class like this:

<class name="Structura.Utils.Tests.Entities.Data, Structura.Utils.Tests" table="tblTESTDATA" lazy="false">
    <id name="Id" column="ID" type="guid" unsaved-value="00000000-0000-0000-0000-000000000000">
        <generator class="guid" />
    </id>
    <property name="Data1" column="DATA1" type="string" length="100" />
    <property name="ExtData">
      <column name="EXTDATA" sql-type="xml" />
      <type name="SID.NHibernateUtils.UserTypes.XmlFieldUserType, SID.NHibernateUtils">
         <param name="knowntypes">Structura.Utils.Tests.Entities.ExtData, Structura.Utils.Tests;Structura.Utils.Tests.Entities.ExtData2, Structura.Utils.Tests</param>
      </type>
    </property> 
</class>


Datacontractserializer, Nhibernate, Serialization, Xml

1 comments

Related Post

  1. #1 da Tomasz Modelski - Saturday March 2010 alle 01:15

    Alessandro, thanks for sharing this. I've used your solution in my current project, 2 minor tweaks were needed, but it works in general. regards, Tomasz.

All fields are required and you must provide valid data in order to be able to comment on this post.


(will not be published)
(es: http://www.mysite.com)