Search This Blog

2010-05-05

.NET + XPath + Namespaces (Conclusion)

In my last post, I discussed how XML namespaces were interfering with XPath expressions that were being used by an application to map XML data. Thanks to kind people on #xml on irc.freenode.net I finally made sense of it.

Within an XSLT document, which you of course know is an XML document, XPath expressions can apparently use the namespaces defined in the XSLT document (I assume then that they can't use namespaces defined in the transforming XML; confirmed). I think that means that if I have the following XML document:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<root>
    <level1 xmlns:foo="http://www.bamccaig.com/foo">
        <level2>Text</level2>
    </level1>
</root>
 
 

Then I could use an XSLT document like this to transform it:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:bar="http://www.bamccaig.com/foo">
    <xsl:template match="//bar:level2">
        <xsl:value-of select="text()" />
    </xsl:template>
</xsl:stylesheet>
 
 

Note that the XPath expression used for the <xsl:template> element's match attribute...

//bar:level2

...uses the namespace prefix defined in the XSLT document (i.e., bar), not the namespace prefix defined in the XML document (i.e., foo).

What this helped me to realize is that it doesn't matter that the XML source document that I'm querying for data with XPath has child elements with default namespaces (i.e, no prefixes) because the prefixes can come from elsewhere. It was then that the .NET API began to make sense. In a way, my .NET is taking the place of the XSLT document, and it then makes sense to define namespaces in .NET. I tried to avoid this originally because it felt like hard-coding what should be soft-coded, but now it makes sense.

In my last post, I mentioned that the XPathNavigator.Evaluate method accepted an IXmlNamespaceResolver. While the XPathNavigator is itself an IXmlNamespaceResolver, it doesn't make sense that it could resolve our namespace (since the namespace in the given example is a default namespace, with no prefix).

So just like in the XSLT, we're going to define a namespace prefix for the namespace used in the XML. We can do that with an XmlNamespaceManager (which implements IXmlNamespaceResolver). Unfortunately, and it escapes me why, we need to pass an XmlNameTable to the XmlNamespaceManager's constructor. Both XmlDocument and XPathNavigator have NameTable properties of that type, but we've already concluded that those won't help us here. Why is it required? I don't know, but it is. With that information, we can finally resolve our namespace:

1
2
3
4
5
6
7
8
9
10
11
var nav = ...; // Our XPathNavigator.
var nsMan = new XmlNamespaceManager(nav.NameTable);
 
nsMan.AddNamespace("bar", "http://www.bamccaig.com/foo");
 
...
 
var it = nav.Evaluate("/root/bar:level1/bar:level2/text()",
        nsMan) as XPathNodeIterator;
 
 

There is one other thing you might be interested in. The data contract API that we're working with (from the post, LINQ to SQL + Serialization) adds a default namespace to the serialization based on the .NET namespace of the serialized type. This happens to be of the form[1],...

http://schemas.datacontract.org/2004/07/Clr.Namespace

...where the namespace is Clr.Namespace. If you don't like that then there are various ways to control it. You can pass a named parameter into the DataContract attribute, however, since in my case the types (and attributes) are generated for me, I don't have control of what parameters are passed to the attribute. The alternative solution that I preferred (because it was easy) was to set an assembly attribute.[1]

Essentially, I opened up the Properties/AssemblyInfo.cs file that Visual Studio had generated for me automatically and added the following line(s):

1
2
3
4
5
// Setting the data contract namespace for the assembly...
// See http://msdn.microsoft.com/en-us/library/ms731045.aspx.
[assembly: ContractNamespace(MyNamespace.ContractNamespace.Uri,
        ClrNamespace = "MyNamespace")]
 

Note: You'll also need a using directive for the System.Runtime.Serialization namespace, but that should be a given. ;) The latest Visual Studio should resolve that for you if you ask it to politely. Alternatively, you could fully-qualify the attribute.

Note that instead of using a string literal, which I could have done, I elsewhere declared a constant field that could be referenced (both here, and earlier, when creating the XmlNamespaceManager), which makes it a lot easier to change the namespace URI in the future.

And that's all there is to it. Let me know if this does or doesn't work for you, or if you have any questions, concerns, or advice. :P

References

  1. http://msdn.microsoft.com/en-us/library/ms731045.aspx

2 comments:

  1. Thanks Brandon, this has really helped me out with understanding how to use IXmlNamespaceResolver.

    ReplyDelete
  2. Glad to hear it. :) Thanks for letting me know.

    ReplyDelete