Correct that there are any number of XPath expressions that result in the same node in the instance document. The simplest way to build an expression that uniquely leads to a specific node is a chain of node tests that use the position of the node in the predicate, for example:
/node()[0]/node()[2]/node()[6]/node()[1]/node()[2]
Obviously, this expression does not use element names, but if all you are trying to do is find the node in the document, you do not need its name. It also cannot be used to search for attributes (since attributes are not nodes and have no position; you can find them only by name), but it will find all other types of nodes.
To build this expression, you need to write a method that returns the position of the node in its parent child nodes, because the XmlNode does not represent this as a property:
static int GetNodePosition(XmlNode child) { for (int i=0; i<child.ParentNode.ChildNodes.Count; i++) { if (child.ParentNode.ChildNodes[i] == child) {
(There is probably a more elegant way to do this with LINQ, since the XmlNodeList implements IEnumerable , but I'm going to use what I know here.)
Then you can write a recursive method as follows:
static string GetXPathToNode(XmlNode node) { if (node.NodeType == XmlNodeType.Attribute) { // attributes have an OwnerElement, not a ParentNode; also they have // to be matched by name, not found by position return String.Format( "{0}/@{1}", GetXPathToNode(((XmlAttribute)node).OwnerElement), node.Name ); } if (node.ParentNode == null) { // the only node with no parent is the root node, which has no path return ""; } // the path to a node is the path to its parent, plus "/node()[n]", where // n is its position among its siblings. return String.Format( "{0}/node()[{1}]", GetXPathToNode(node.ParentNode), GetNodePosition(node) ); }
As you can see, I hacked it to find attributes.
John joined his version while I wrote mine. There is something in his code that will make me hot now, and I apologize in advance if it sounds like I'm scolding John. (I'm not sure. I'm pretty sure that the list of what John should learn from me is extremely short.) But I think that the thought I'm going to do is pretty important for those who work with XML to think about.
I suspect that the Jon solution came from what I see many developers do: treating XML documents as trees of elements and attributes. I think this comes largely from developers whose main use of XML is a serialization format, because all of the XML they use is structured this way. You may notice these developers because they use the terms “node” and “element” interchangeably. This forces them to propose solutions that treat all other types of nodes as special cases. (I was one of these guys myself for a very long time.)
This seems like a simplifying assumption while you do this. But this is not so. This complicates the tasks and complicates the code. This forces you to bypass parts of XML technology (for example, the XPath node() function) that are specifically designed to handle all types of nodes together.
There is a red flag in Jon code that forces me to request it in a code review, even if I don’t know what the requirements are, and what GetElementsByTagName . Whenever I see that this method is being used, the question arises: "Why should it be an element?". And the answer is very often "oh, should this code handle text nodes too?"