Working with XML Fields in NHibernate

Proposed solution involves three steps:

  1. Create a type XmlData which will transparently convert string to XML data on field loading, and XML data back to the string when field must be updated.
  2. Create the custom type, which will load string into XmlData objects and save XmlData objects to strings.
  3. Create a property of XmlData type and map it with your custom type specified.

Some additional features:

  1. XmlData field converts string to XML only when it's first requested. Original string data is erased only if the XML node is changed.
  2. XmlData has XmlNamespaceManager that holds namespaces and prefixes that are considered implicit for this data field. In other words it doesn't write declarations of known namespaces to the database field and doesn't require those declarations upon loading data from the database.
  3. The solution is ObjectXPathNavigator-friendly, and XmlData property will be mapped as nested XML content. Last version of sdf.XPath could be taken here. For more information consult whole blog category.

 

Classes

XmlData.cs - main class of the solution. To be used as the type for a XML-valued property.

using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using sdf.XPath;

namespace sdf.Persist
{
    public class XmlData
    {
        private string _stringData = null;
        XmlDocument _doc = null;
        private XmlElement _xmlData = null;
        private XmlNamespaceManager _nsmgr = null;

        public XmlData()
        {
        }

        public XmlData( XmlNamespaceManager nsmgr )
        {
            _nsmgr = nsmgr;
        }

        [XmlIgnore]
        public string String
        {
            get
            {
                if( _stringData == null ) 
                    XmlToString();
                return _stringData;
            }
            set
            {
                _stringData = value;
                _xmlData = null;
                Unsubscribe();
            }
        }

        [XmlAnyElement, SkipNavigableRoot]
        public XmlElement Xml
        {
            get
            {
                if( _xmlData == null )
                    StringToXml();
                return _xmlData;
            }
        }

        [XmlIgnore]
        public XmlDocument Doc
        {
            get { return _doc; }
        }

        public void StringToXml()
        {
            // Unsubscribe from events
            Unsubscribe();

            // Create new document
            if( _nsmgr != null )
                _doc = new XmlDocument( _nsmgr.NameTable );
            else
                _doc = new XmlDocument();

            _xmlData = _doc.CreateElement( "xml" );
    
            if( _stringData != null && _stringData != string.Empty )
            {
                // Load XML from string
                XmlParserContext context = new XmlParserContext( null, _nsmgr, null, XmlSpace.Default );
                XmlTextReader reader = new XmlTextReader( _stringData, XmlNodeType.Element, context );
                do
                {
                    XmlNode nextChild = _doc.ReadNode( reader );
                    if( nextChild != null )
                        _xmlData.AppendChild( nextChild );
                    else
                        break;
                } 
                while( true );
            }
    
            // Subscibe document change events
            Subscribe();
        }

        public void XmlToString()
        {
            if( _xmlData != null ) 
            {
                StringWriter sw = new StringWriter();
                SkipNsXmlTextWriter xtw = new SkipNsXmlTextWriter( sw, _nsmgr );
                _xmlData.WriteContentTo( xtw );
                xtw.Close();
                _stringData = sw.ToString();
            }
            else
                _stringData = string.Empty;
        }

        [XmlIgnore]
        public XmlNamespaceManager NamespaceManager
        {
            get { return _nsmgr; }
            set { _nsmgr = value; }
        }

        public string ValuleOf( string xpath ) 
        {
            return ValuleOf( xpath, NamespaceManager, "" );
        }

        public string ValuleOf( string xpath, XmlNamespaceManager nsmgr ) 
        {
            return ValuleOf( xpath, nsmgr, "" );
        }

        protected string ValuleOf( string xpath, XmlNamespaceManager nsmgr, string defaultValue ) 
        {
            if ( Xml.FirstChild == null )
                return defaultValue;

            XmlNode node = Xml.SelectSingleNode( xpath, nsmgr );

            if ( node == null )
                return defaultValue;

            if ( node is XmlElement )
                return node.InnerText;

            if ( node is XmlAttribute )
                return node.Value;


            return node.Value;
        }

        public static explicit operator string( XmlData data )
        {
            return data.String;
        }

        public static explicit operator XmlElement( XmlData data )
        {
            return data.Xml;
        }


        private void Subscribe()
        {
            _doc.NodeChanged += new XmlNodeChangedEventHandler( OnDocumentChange );
            _doc.NodeInserted += new XmlNodeChangedEventHandler( OnDocumentChange );
            _doc.NodeRemoved += new XmlNodeChangedEventHandler( OnDocumentChange );
        }

        private void Unsubscribe()
        {
            if( _doc != null )
            {
                _doc.NodeChanged -= new XmlNodeChangedEventHandler( OnDocumentChange );
                _doc.NodeInserted -= new XmlNodeChangedEventHandler( OnDocumentChange );
                _doc.NodeRemoved -= new XmlNodeChangedEventHandler( OnDocumentChange );
            }
        }

        private void OnDocumentChange( object sender, XmlNodeChangedEventArgs e )
        {
            _stringData = null;
        }
    }
}

 

XmlDataType.cs - custom NHibernate type that interfaces XmlData instance and a database.

using System;
using System.Data;
using log4net;
using NHibernate;
using NHibernate.SqlTypes;

namespace sdf.Persist.Hibernate
{
    public class XmlDataType : IUserType
    {
        private static readonly ILog log = LogManager.GetLogger( typeof( XmlDataType ) );

        private static SqlType[] _sqlTypes = new SqlType[] { new StringClobSqlType() };
        
        public object NullSafeGet( IDataReader rs, string[] names, object owner )
        {
            string text;
            object value = rs.GetValue( rs.GetOrdinal( names[0] ) );
            if( value == DBNull.Value )
                return null;

            text = (string)value;
            XmlData data = new XmlData();
            data.String = text;
            return data;
        }

        public void NullSafeSet( IDbCommand cmd, object value, int index )
        {
            if( value != null )
            {
                string str = ((XmlData)value).String;
                if( str.Length > 0) 
                {
                    ((IDataParameter)cmd.Parameters[index]).Value = str;
                    return;
                }
            }

            ((IDataParameter)cmd.Parameters[index]).Value = DBNull.Value;
        }

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

            XmlData data = (XmlData)value;
            XmlData copy = new XmlData( data.NamespaceManager );
            copy.String = data.String;
            return copy;
        }

        public SqlType[] SqlTypes
        {
            get { return _sqlTypes; }
        }

        public Type ReturnedType
        {
            get { return typeof( XmlData ); }
        }

        public bool IsMutable
        {
            get { return true; }
        }

        bool IUserType.Equals( object x, object y )
        {
            if( x == null && y == null )
                // Both of them are null.
                return true;
            
            XmlData d1 = x as XmlData;
            XmlData d2 = y as XmlData;

            if( d1 == null || d2 == null )
                // 1. Both are XmlData, but not both null.
                // 2. One of given files is not of the right type.
                return false;

            return d1.String.Equals( d2.String );
        }
    }
}

 

SkipNsXmlTextWriter.cs - service type that allows for skipping unnecessary namespaces while converting XML node to string.

using System.IO;
using System.Xml;

namespace sdf.Persist
{
    /// <summary>
    /// Writes XML to the given <see cref="TextWriter"/>, omitting declarations 
    /// of namespaces presented in specified <see cref="XmlNamespaceManager"/>.
    /// </summary>
    public class SkipNsXmlTextWriter : XmlTextWriter
    {
        private XmlNamespaceManager _nsmgr;
        private bool _catchNs;
        private string _prefix;
        private string _ns;
        private string _forbiddenNs;

        public SkipNsXmlTextWriter( TextWriter writer, XmlNamespaceManager nsmgr ):
            base( writer )
        {
            _nsmgr = nsmgr;
            _catchNs = false;
            _prefix = null;
            _ns = null;
        }

        public override void WriteStartAttribute( string prefix, string localName, string ns )
        {
            if( _nsmgr != null ) 
            {
                if( prefix == "xmlns" )
                {
                    if( localName != string.Empty && localName != null ) 
                    {
                        _forbiddenNs = _nsmgr.LookupNamespace( localName );
                        if( _forbiddenNs != null )
                        {
                            _catchNs = true;
                            _prefix = localName;
                            _ns = null;
                            return;
                        }
                    } 
                }
                else if( ( prefix == null || prefix == string.Empty ) && localName == "xmlns" )
                {
                    _catchNs = true;
                    _prefix = null;
                    _ns = null;
                    return;
                }
            }
            base.WriteStartAttribute( prefix, localName, ns );
        }

        public override void WriteEndAttribute()
        {
            if( _catchNs )
            {
                _catchNs = false;
                _ns = _nsmgr.NameTable.Get( _ns );
                if( _prefix != null ) 
                {
                    if( _forbiddenNs == _ns )
                        // If xmlns:ns="the-ns" where namespace is given
                        return;
                }
                else if( _nsmgr.LookupPrefix( _ns ) != null )
                    // If xmlns="the-ns" whrer namespace is given
                    return;

                base.WriteStartAttribute( "xmlns", _prefix, "http://www.w3.org/2000/xmlns/" );
                base.WriteString( _ns );
            }
            base.WriteEndAttribute();
        }

        public override void WriteString( string text )
        {
            if( _catchNs )
            {
                if( _ns == null )
                    _ns = text;
                else
                    _ns += text;
                return;
            }
            base.WriteString( text );
        }

        public override void WriteStartElement( string prefix, string localName, string ns )
        {
            if( _nsmgr != null && ns != string.Empty && ns != null ) 
            {
                if( prefix != string.Empty && prefix != null ) 
                {
                    if( _nsmgr.LookupNamespace( prefix ) == ns ) 
                    {
                        base.WriteStartElement( null, prefix + ":" + localName, null );
                        return;
                    }
                }
                else
                {
                    string givenPrefix = _nsmgr.LookupPrefix( ns );
                    if( givenPrefix != null ) 
                    {
                        base.WriteStartElement( null, givenPrefix + ":" + localName, null );
                        return;
                    }
                }
            }
            
            base.WriteStartElement( prefix, localName, ns );
        }

    }
}

Example

In your class define the property of XmlData type:

public class MyClass
{
    private XmlData _data;

    ...

    public XmlData Data
    {
        get
        {
            // Create empty data if accessed first time 
            // (for example, to use in newly created objects)
            if( _data == null )
                _data = new XmlData();
            return _data;
        }
        set { _data = value; }
    }
    
    ...

}

 

Map it with custom converter type specified:

<class name="myns.MyClass, myasm">

    ...

    <property name="OtherData" column="otherData" 
        access="field.camelcase-underscore" type="sdf.Persist.Hibernate.XmlDataType, sdf" />

    ...
</class>