High-performance XML (III): returning well-formed XML from WebServices without XmlDocument
Note: this entry has moved.
Recently, Matt Powell wrote about returning XML from webservices, and I certainly agree with him that returning it as an opaque string is really bad. Later on, Matevz Gacnik suggested a couple points to consider when to choose one or the other. Matt continued his rant this time tackling at the heart of the problem: why do you need to load a full-blown DOM just to get the nice XML returned from the webservice? At this point, I felt I should kick in.
You see, one of the greatest things about having a fully OO platform is that you can easily complement what's missing by simply inheriting a couple classes, plugging your stuff in the infrastructure. So, let me state it clear and loud: you don't have to load an XmlDocument to return well-formed XML from webservices. Let's see how this is accomplished.
As you may already know, the ASP.NET WebMethod framework uses the XmlSerializer to convert arguments and return values to their XML representations. When your webmethod returns an XmlDocument, it will serialize the XmlDocument.DocumentElement, effectively the root element. If you return an XmlNode, it will simply serialize it. So, you could either return an XmlDocument *if you already have it at hand*, or an XmlNode (this could even be the DocumentElement of the previous one). The client will see the same WSDL and the Visual Studio Add Web Reference feature will generate a proxy returning an XmlNode in either case. So, *both* methods below result in the same proxy class:
[WebMethod]
public XmlDocument MyDocumentMethod()
{
//...
}
[WebMethod] public XmlNode MyNodeMethod()
{
//...
}
// Proxy code
MyService proxy = new MyService();
XmlNode xml = proxy.MyDocumentMethod();
// or xml = proxy.MyNodeMethod();
When the XmlSerializer used to build the SOAP Body for our return value realizes it's an XmlDocument/XmlNode, it will simply call its WriteTo(XmlWriter w) method. So, what can we do with all this knowledge? Well, we can leverage it to avoid loading full DOMs when we have other (better IMO) representations such as an XmlReader or an XPathNavigator. The process is simply to create a special-purpose XmlNode-derived class that will serve as our trojan horse (very appropriate now that Troy is in vogue ;)) into the webservice serialization infrastructure.
Implementation
Implementing the XmlNode.WriteTo() method with an XmlReader is all too easy:
private class XmlReaderNode : SerializableNode
{
private XmlReader _reader;
private bool _default;
public XmlReaderNode() {}
public XmlReaderNode(XmlReader reader, bool defaultAttrs)
{
_reader = reader;
_reader.MoveToContent();
_default = defaultAttrs;
}
public override void WriteTo(XmlWriter w)
{
w.WriteNode(_reader, _default);
_reader.Close();
}
}
Note that we need to move the reader to the actual content, because we don't have to serialize the document declaration again, because the result is placed inside the SOAP body. We'll get to the base SerializableNode class in a minute. Basically it overrides everything and throws NotSupportedExceptions.
Implementing the XmlNode.WriteTo() method with an XPathNavigator is also easy, as we can take advantage of the Mvp.Xml project XPathNavigatorReader:
private class XPathNavigatorNode : SerializableNode
{
private XPathNavigator _navigator;
public XPathNavigatorNode() {}
public XPathNavigatorNode(XPathNavigator navigator)
{
_navigator = navigator;
}
public override void WriteTo(XmlWriter w)
{
w.WriteNode(new XPathNavigatorReader(_navigator), false);
}
}
Note that in both cases, we use the built-in infrastructure by calling the XmlWriter.WriteNode() method. Specially in the case of the XmlReaderNode, that means we're never building an in-memory DOM. In the case of the XPathNavigatorNode it means that we're working with the existing in-memory infoset, and we're not loading another one for the serialization.
The only "trick" in the base SerializableNode is in its constructor, because the base XmlNode and XmlElement don't allow empty constructors. The workaround was to create an element with a dummy name and an empty owner document:
private abstract class SerializableNode : XmlElement
{
public SerializableNode() : base("", "dummy", "", new XmlDocument()) {}
public override XmlNode AppendChild(XmlNode newChild)
{
throw new NotSupportedException(SR.XmlDocumentFactory_NotImplementedDOM);
}
//...all other members...
}
At this point you may be wondering why all these classes are private. Well, in order to isolate the user from the internal implementation details of these classes, I prefer to create a factory class that simply returns XmlNode values:
public class XmlNodeFactory
{
private XmlNodeFactory() {}
public static XmlNode Create(XPathNavigator navigator)
{
return new XPathNavigatorNode(navigator);
}
public static XmlNode Create(XmlReader reader)
{
return Create(reader, false);
}
public static XmlNode Create(XmlReader reader, bool defaultAttrs)
{
return new XmlReaderNode(reader, defaultAttrs);
}
//...private inner classes follow...
}
Usage
Now, let's get back to your webservice code. Most probably, specially if you have been reading my weblog and many others that advice against the XmlDocument, you'll already have an XmlReader or XPathNavigator you got from your business classes or your data access layer. With that at hand, you can simply use the following code:
[WebMethod]
public XmlNode GetFromNavigator()
{
XPathNavigator nav;
// Get your navigator...
return XmlNodeFactory.Create(nav);
}
[WebMethod]
public XmlNode GetFromReader()
{
XmlReader reader;
// Get your reader, maybe even from SQL Server?...
return XmlNodeFactory.Create(reader);
}
Note that I mention getting the reader from SQL Server. If you look at the XmlReaderNode code shown in the previous section, you'll notice that once the WriteTo() serialization method is invoked, the reader is closed.
The interesting thing in the XPathNavigator case is that you can position the navigator on the node you want to return, and have only that "fragment" serialized. For example:
[WebMethod]
public XmlNode GetFromNavigator()
{
string xml = "<customer><order>25</order><info>Daniel Cazzulino</info></customer>";
XPathDocument doc = new XPathDocument(new StringReader(xml));
XPathNavigator nav = doc.CreateNavigator();
//Move to the customer node
nav.MoveToFirstChild();
//Move to the order node
nav.MoveToFirstChild();
return XmlNodeFactory.Create(nav);
}
The returned data will be only the <order>25</order>
element (and any children it may have). Cool, huh? That's XPathNavigatorReader courtesy ;)
The full project source code belongs to the Mvp.Xml project and can be downloaded from SourceForge. Enjoy!
Check out the Roadmap to high performance XML.