tags:

views:

136

answers:

3

I just ran into an issue where my code was parsing xml fine but once I added in a second node it started to load incorrect data. The real code spans a number of classes and projects but for the sample I've put together the basics of what's causing the issue

When the code runs I'd expect the output to be the contents of the second Task node, but instead the contents of the first node is output. It keeps pulling from the first occurrence of the EmailAddresses node despite how when you check the settings object its inner xml is that of the second Task node. The call to SelectSingleNode("//EmailAddresses") is where the issue happens.

I have two ways around this issue

  1. Remove the leading slashes from the EmailAddresses XPath expression
  2. Call Clone() after getting the Task or Settings node

Solution 1 works in this case but I believe this will cause other code in my project to stop working.

Solution 2 looks more like a hack to me than a real solution.

MY question is am I in fact doing this correctly and there's a bug in .NET (all versions) or am I just pulling the XML wrong?

The c# code

var doc = new XmlDocument();
doc.Load(@"D:\temp\Sample.xml");

var tasks = doc.SelectSingleNode("Server/Tasks");

foreach (XmlNode threadNode in tasks.ChildNodes)
{
    if (threadNode.Name.ToLower() != "thread")
    {
        continue;
    }

    foreach (XmlNode taskNode in threadNode.ChildNodes)
    {
        if (taskNode.Name.ToLower() != "task" || taskNode.Attributes["name"].Value != "task 1")
        {
            continue;
        }

        var settings = taskNode.SelectSingleNode("Settings");

        var emails = settings.SelectSingleNode("//EmailAddresses");

        Console.WriteLine(emails.InnerText);
    }
}

The XML

<?xml version="1.0"?>
<Server>
    <Tasks>
        <Thread>
            <Task name="task 2">
                <Settings>
                    <EmailAddresses>task 2 data</EmailAddresses>
                </Settings>
            </Task>
        </Thread>
        <Thread>
            <Task name="task 1">
                <Settings>
                    <EmailAddresses>task 1 data</EmailAddresses>
                </Settings>
            </Task>
        </Thread>
    </Tasks>
</Server>
+3  A: 

The // XPath expression does not do what you think it does. It selects nodes in the document from the current node that match the selection no matter where they are.

In other words, it's not limited by the current scope, it actually crawls back up the document tree and starts matching from the root element.

To select the first <EmailAddresses> element in your current scope, you only need:

var emails = settings.SelectSingleNode("EmailAddresses");
Frédéric Hamidi
A: 

// comes for search and it will return all nodes related and when using single node returns first one, but in this case you have just one node why you didn't use settings.SelectSingleNode("EmailAddresses")?

SaeedAlg
The call to get the EmailAddresses node is located in a shared method that's used for something else. The issue looks to be simply that I'm reusing code that doesn't fit this scenario despite how it looks like it does.
Brian Surowiec
+5  A: 

From http://www.w3.org/TR/xpath/#path-abbrev

// is short for /descendant-or-self::node()/. For example, //para is short for /descendant-or-self::node()/child::para and so will select any para element in the document (even a para element that is a document element will be selected by //para since the document element node is a child of the root node);

And also:

A location step of . is short for self::node(). This is particularly useful in conjunction with //. For example, the location path .//para is short for

self::node()/descendant-or-self::node()/child::para

and so will select all para descendant elements of the context node.

Instead of:

var settings = taskNode.SelectSingleNode("Settings");

var emails = settings.SelectSingleNode("//EmailAddresses");

Use:

var emails = taskNode.SelectSingleNode("Settings/EmailAddresses");
Alejandro