Developer Blog Search

Monday 12 May 2008

Xml serialization of objects with circular references

I always felt this was missing from XmlSerializer.

Then like magic, along came DataContracts, which can be used to do roughly the same thing, except you now have the added option of preserveObjectReferences as a parameter of the constructor.

Great, this solves the problem right? well, yes but it also raises more :) the resulting xml for a simple structure like this:

public class Site { Page[] pages; string name; } public class Page { Site site; string name; }

This looks more complex because it uses reference attributes. Every serialized object is given an ID attribute. If that object is serialized again (i.e. from a circular reference) it will just use an attribute that says "i'm really object 54".

<a:Site z:Id="1" xmlns="urn:test" xmlns:a="http://schemas.datacontract.org/2004/07/EmitTest" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/"> <a:Name z:Id="3">Site 1</a:Name> <a:Pages z:Id="4" z:Size="4"> <a:Page z:Id="5"> <a:Name z:Id="6">Page 1</a:Name> <a:Site z:Ref="1" i:nil="true" /> </a:Page> <a:Page z:Id="8"> <a:Name z:Id="9">Page 2</a:Name> <a:Site z:Ref="1" i:nil="true" /> </a:Page> <a:Page z:Id="11"> <a:Name z:Id="12">Page 3</a:Name> <a:Site z:Ref="1" i:nil="true" /> </a:Page> <a:Page z:Id="14"> <a:Name z:Id="15">Page 4</a:Name> <a:Site z:Ref="1" i:nil="true" /> </a:Page> </a:Pages> </a:Site>

This makes it rather difficult to do XSLT and XPath with the result. So unless you don't mind resolving every node in a query, I've written implementations of XPathNavigator and XmlDocument to transparently resolve these references for you.

using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml.XPath; using System.Reflection; using System.Xml; namespace EmitTest { public class DataContractXPathNavigator : XPathNavigator, IHasXmlNode { private XPathNavigator _nav; private XmlDocument _doc; public DataContractXPathNavigator(XPathNavigator nav, XmlDocument doc) { _nav = nav; _doc = doc; } public override string BaseURI { get { return _nav.BaseURI; } } public override XPathNavigator Clone() { return new DataContractXPathNavigator(_nav.Clone(), _doc); } public override bool IsEmptyElement { get { return _nav.IsEmptyElement; } } public override bool IsSamePosition(XPathNavigator other) { return _nav.IsSamePosition(other); } public override string LocalName { get { return _nav.LocalName; } } public override bool MoveTo(XPathNavigator other) { bool result = _nav.MoveTo(other); ResolveReference(); return result; } public override bool MoveToFirstAttribute() { return _nav.MoveToFirstAttribute(); } public override bool MoveToFirstChild() { bool result = _nav.MoveToFirstChild(); ResolveReference(); return result; } public override bool MoveToFirstNamespace(XPathNamespaceScope namespaceScope) { return _nav.MoveToFirstNamespace(namespaceScope); } public override bool MoveToId(string id) { return _nav.MoveToId(id); } public override bool MoveToNext() { bool result = _nav.MoveToNext(); ResolveReference(); return result; } public override bool MoveToNextAttribute() { return _nav.MoveToNextAttribute(); } public override bool MoveToNextNamespace(XPathNamespaceScope namespaceScope) { return _nav.MoveToNextNamespace(namespaceScope); } public override bool MoveToParent() { return _nav.MoveToParent(); } public override bool MoveToPrevious() { bool result = _nav.MoveToPrevious(); ResolveReference(); return result; } public override string Name { get { return _nav.Name; } } public override System.Xml.XmlNameTable NameTable { get { return _nav.NameTable; } } public override string NamespaceURI { get { return _nav.NamespaceURI; } } public override XPathNodeType NodeType { get { return _nav.NodeType; } } public override string Prefix { get { return _nav.Prefix; } } public override string Value { get { return _nav.Value; } } private void ResolveReference() { try { string refattr = _nav.GetAttribute("Ref", "http://schemas.microsoft.com/2003/10/Serialization/"); if (refattr != String.Empty) { XPathNavigator refnode = _nav.SelectSingleNode("//*[@" + _nav.LookupPrefix("http://schemas.microsoft.com/2003/10/Serialization/") + ":Id='" + refattr + "']", this); if (refnode != null) { MoveTo(refnode); } else { throw new InvalidReferenceException(refattr); } } } catch (StackOverflowException ex) { throw new CircularTemplateReferenceException(_nav.Name); } } #region IHasXmlNode Members public XmlNode GetNode() { return ((IHasXmlNode)_nav).GetNode(); } #endregion } public class InvalidReferenceException : Exception { public InvalidReferenceException(string referenceId) : base("Unable to resolve reference " + referenceId + ".") { } } public class CircularTemplateReferenceException : Exception { public CircularTemplateReferenceException(string nodeName) : base("A circular reference was detected on node " + nodeName + ".") { } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml; using System.IO; using System.Runtime.Serialization; namespace EmitTest { public class DataContractXmlDocument : XmlDocument { private XmlNamespaceManager _nsm = null; public DataContractXmlDocument(object root) { DataContractSerializer dcs = new DataContractSerializer(typeof(Site), "a:" + root.GetType().Name, "", null, 200, true, true, null); MemoryStream ms = new MemoryStream(); dcs.WriteObject(ms, root); LoadXml(Encoding.UTF8.GetString(ms.ToArray())); } public override System.Xml.XPath.XPathNavigator CreateNavigator() { return new DataContractXPathNavigator(base.CreateNavigator(), this); } public virtual new XmlNode SelectSingleNode(string xpath) { if (_nsm == null) { _nsm = new XmlNamespaceManager(NameTable); foreach (XmlAttribute attr in base.SelectSingleNode("/*").Attributes) { if (attr.Prefix == "xmlns") { _nsm.AddNamespace(attr.LocalName, attr.Value); } } } return base.SelectSingleNode(xpath, _nsm); } } }

This now makes it possible to do the following with your serialized objects:

Console.WriteLine(doc.SelectSingleNode("/a:Site/a:Pages/a:Page[1]/a:Site/a:Name").InnerXml);

remember though, this also allows you to get your xslt in an infinite loop easily. be careful.

No comments: