1

I'm trying to parse a valid XML document in c# .NET using xPathNavigator. The start of the XML document looks like this:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <soapenv:Body>
        <ns1:myResponse xmlns:ns1="ExampleNS" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
            <myReturn href="#2" />

As you can see, there's some namespaces defined on the root element, but then ns1 is defined later on. When I try and evaluate an xPath query /soapenv:Envelope/soapenv:Body/ns1:myResponse/myReturn I get an XPathException:

Namespace prefix 'ns1' is not defined.

What I'm doing is getting the XML document as a string, loading it into an XmlDocument object. Then I'm creating a navigator, moving to the root element and calling GetNamespacesInScope(XmlNamespaceScope.All). I loop through this collection, which contains the namespaces for xml, soapenv, xsd and xsi as defined on the root, adding them to the namespacemanager. Then I create an xPathNavigator and call Evaluate, and get the exception.

Code is this:

string response = GetXMLString();
var xmlDocument = new XmlDocument();
xmlDocument.LoadXml(response);

var xmlDocumentNavigator = xmlDocument.CreateNavigator();
xmlDocumentNavigator.MoveToFollowing(XPathNodeType.Element);
var xnm = new XmlNamespaceManager(xmlDocument.NameTable);
var namespacesInScope = xmlDocumentNavigator.GetNamespacesInScope(XmlNamespaceScope.All);
if (namespacesInScope != null)
{
    foreach (var prefix in namespacesInScope.Keys)
    {
        xnm.AddNamespace(prefix, namespacesInScope[prefix]);
    }
}

var xPathDocument = new XPathDocument(new StringReader(response));
var xPathNavigator = xPathDocument.CreateNavigator();

xPathNavigator.Evaluate("/soapenv:Envelope/soapenv:Body/ns1:myResponse/myReturn", xnm);

Why doesn't the GetNamespacesInScope method pick up on ns1?

growse
  • 3,554
  • 9
  • 43
  • 66
  • Never liked how namespaces where declared in XML, one of them for the case you presented. Anyway, its common practice, maybe not a standard, that xml all namespaces should be declared before used, so the document you are using, maybe considered "damaged". – umlcat Sep 12 '12 at 15:43

2 Answers2

3

The problem is that GetNamespacesInScope only returns the namespaces in the scope of the navigator's current node (as I guess the method suggests :).

One hack of getting all the Namespaces is to walk all the elements, get the namespaces, and then reassemble just the unique ones.

Dictionary merge code courtesy of JonSkeet

 XPathDocument xpd = new XPathDocument(new StringReader(s));
 XPathNavigator xpn = xpd.CreateNavigator();
 List<IDictionary<string, string>> xmlnsList = new List<IDictionary<string,string>>();
 while (xpn.MoveToFollowing(XPathNodeType.Element))
 {
     xmlnsList.Add(xpn.GetNamespacesInScope(XmlNamespaceScope.All));
 }
 var result = xmlnsList.SelectMany(dict => dict)
              .ToLookup(pair => pair.Key, pair => pair.Value)
              .ToDictionary(group => group.Key, group => group.First());
 if (result != null)
 {
     foreach (var prefix in result.Keys)
     {
         xnm.AddNamespace(prefix, result[prefix]);
     }
 }

Edit

As per Paciv's comment, if you really don't know what the namespaces are in an xml document in advance, an alternative is to use namespace agnostic Xpath, and avoid the issues around sniffing out namespaces (and eliminating the need for namespace managers entirely).

In your example, this would be:

xPathNavigator.Evaluate("/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='myResponse']/*[local-name()='myReturn']");
Community
  • 1
  • 1
StuartLC
  • 104,537
  • 17
  • 209
  • 285
  • I actually don't know the exact .NET implementation, but to me, the point of the navigator is that you don't have to parse the whole xml to get to a node. And although this solution works it "kills" the concept of requesting by an XPath query on a navigator by forcing a full document parse. – Paciv Sep 12 '12 at 16:30
  • 1
    That works well, unless there are multiple elements at the same level with the same local name under different namespace, but that seldom happens :) – Paciv Sep 12 '12 at 16:42
2

What about just adding the namespace you know beforehand that you will need to execute the XPath query ?

You could just write

xnm.AddNamespace("MyNS", "ExampleNS");

And then

xPathNavigator
    .Evaluate("/soapenv:Envelope/soapenv:Body/MyNS:myResponse/myReturn", xnm);

It shouldn't matter what the prefix is as long as the namespace designed by the prefix is the same.

By the way, you should do the same for the SOAP namespace, because you're basing your XPath query on prefixes, and one could generate the exact same XML with different prefixes targeting the same namespaces.

I would write something more robust, applicable on different SOAP generators like

string response = GetXMLString();
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.LoadXml(response);
XmlNamespaceManager xnm = new XmlNamespaceManager(xmlDocument.NameTable);
xnm.AddNamespace("soapenv", "http://schemas.xmlsoap.org/soap/envelope/");
xnm.AddNamespace("ns1", "ExampleNS");

xmlDocument.CreateNavigator()
    .Evaluate("/soapenv:Envelope/soapenv:Body/ns1:myResponse/myReturn", xnm);
Paciv
  • 1,487
  • 10
  • 16
  • This isn't a bad idea, my only worry is that the type of xml document I could get is fairly varied, and I don't necessarily know in advance what the namespaces will be. – growse Sep 12 '12 at 16:19
  • 1
    You can't write a XPath query if you don't know what you're requesting. If you know the element name you're targeting, you should also know its namespace. – Paciv Sep 12 '12 at 16:23