XPathNavigator MoveToRoot Gotcha

Most good .NET developers consult the MSDN Library frequently. On the whole, the Library is comprehensive and accurate. Its quality is not quite on the level of UNIX manual pages, but it has improved over the years. Still, one problem persists: too often, the focus is on happy paths, and edge cases are ignored. This results in the occasional nasty surprise.

Such was the cause of a recent bug in my client's product. Let's examine the Library article on the XPathNavigator.MoveToRoot() method:

Moves the XPathNavigator to the root node that the current node belongs to. All nodes belong to one and only one document. Therefore, this method is always successful.

This seems about as unambiguous as it gets. Unfortunately, a key detail is missing. More than one type of node can be considered root, and which one is root in a particular case depends on how the underlying XML document was constructed. For XPathNavigator instances constructed over XmlDocuments, MoveToRoot() can move to either the owning XmlDocument OR the root XmlElement.

In the following example, the navigator initially is positioned on a child node. MoveToRoot() moves to the owning XmlDocument.

[TestMethod]
public void MoveToRoot_MovesToDocument()  
{
    var xml   = new XmlDocument();
    var root  = xml.CreateElement("A");
    var child = xml.CreateElement("B");

    xml.AppendChild(root);
    root.AppendChild(child);

    var nav = child.CreateNavigator();
    nav.MoveToRoot();

    Assert.AreSame(child.OwnerDocument.DocumentElement, root, "DocumentElement");
    Assert.AreEqual(nav.NodeType, XPathNodeType.Root, "NodeType");
    Assert.AreSame(nav.UnderlyingObject, xml, "UnderlyingObject");
}

If we omit the AppendChild() call on the document (something commonly done), MoveToRoot() instead moves to the root XmlElement:

[TestMethod]
public void MoveToRoot_MovesToRootNode()  
{
    var xml   = new XmlDocument();
    var root  = xml.CreateElement("A");
    var child = xml.CreateElement("B");

    //xml.AppendChild(root); // This is the difference!
    root.AppendChild(child);

    var nav = child.CreateNavigator();
    nav.MoveToRoot();

    Assert.IsNull(child.OwnerDocument.DocumentElement, "DocumentElement");
    Assert.AreEqual(nav.NodeType, XPathNodeType.Element, "NodeType");
    Assert.AreSame(nav.UnderlyingObject, root, "UnderlyingObject");
}

It's understandable why this quirk is not documented. XPathNavigator is an abstract class. It has no affinity towards any of the various XML APIs in the Base Class Library. The framework provides a number of concrete subclasses that adapt XPathNavigator to each API. Ideally, per-API edge cases would be documented in the MSDN Library article for each concrete subclass. Those classes, however, aren't public. Since there's no good place to document the quirks, this quirk isn't documented.