Best way to make this LINQ to XML query?

So to speak, I have this XML file:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Root>
  <Category Name="Tasties">
    <Category Name="Pasta">
      <Category Name="Chicken">
        <Recipe Name="Chicken and Shrimp Scampi" />
        <Recipe Name="Chicken Fettucini Alfredo" />
      </Category>
      <Category Name="Beef">
        <Recipe Name="Spaghetti and Meatballs" />
        <Recipe Name="Lasagna" />
      </Category>
      <Category Name="Pork">
        <Recipe Name="Lasagna" />
      </Category>
      <Category Name="Seafood">
        <Recipe Name="Chicken and Shrimp Scampi" />
      </Category>
    </Category>
  </Category>
</Root>

      

And I want to return the names of all the recipes in Tasties \ Pasta \ Chicken, how would I do that?

I currently have:

var q = from chk in
            (from c in doc.Descendants("Category")
             where c.Attribute("Name").Value == "Chicken"
             select c)
        select from r in chk.Descendants("Recipe")
               select r.Attribute("Name").Value;

foreach (var recipes in q)
{
    foreach (var recipe in recipes)
    {
        Console.WriteLine("Recipe name = {0}", recipe);
    }
}

      

Which kind works, although it doesn't check the path, only for the first category called Chicken. I could dig over each element in the path recursively, but it looks like this is probably the best solution I'm missing. Also my current query is returning IEnumerable<IEnumerable<String>>

when all I want is simple IEnumerable<String>

.

Basically I can get it to work, but it looks like a mess and I would like to see any LINQ suggestions or methods to do better queries.

+1


a source to share


4 answers


Personally, I would use the XmlDocument

familiar one too SelectNodes

:

foreach(XmlElement el in doc.DocumentElement.SelectNodes(
   "Category[@Name='Tasties']/Category[@Name='Pasta']/Category[@Name='Chicken']/Recipe")) {
    Console.WriteLine(el.GetAttribute("Name"));
}

      

For LINQ-to-XML, I would suggest (untested) something like:

var q = from c1 in doc.Root.Elements("Category")
        where c1.Attribute("Name").Value == "Tasties"
        from c2 in c1.Elements("Category")
        where c2.Attribute("Name").Value == "Pasta"
        from c3 in c2.Elements("Category")
        where c3.Attribute("Name").Value == "Chicken"
        from recipe in c3.Elements("Recipe")
        select recipe.Attribute("Name").Value;
foreach (string name in q) {
    Console.WriteLine(name);
}

      


Edit: if you want more flexible category selection:

    string[] categories = { "Tasties", "Pasta", "Chicken" };
    XDocument doc = XDocument.Parse(xml);
    IEnumerable<XElement> query = doc.Elements();
    foreach (string category in categories) {
        string tmp = category;
        query = query.Elements("Category")
            .Where(c => c.Attribute("Name").Value == tmp);
    }
    foreach (string name in query.Descendants("Recipe")
        .Select(r => r.Attribute("Name").Value)) {
        Console.WriteLine(name);
    }

      

This should now work for any number of levels, selecting all recipes at or below the selected level.




Edit for discussion (comments) why Where

has a local variable tmp

:

It might be a little tricky, but I'm trying to answer the question: -p

Basically, foreach

(with an lvalue "capture" iterator) looks like this:

class SomeWrapper {
    public string category;
    public bool AnonMethod(XElement c) {
        return c.Attribute("Name").Value == category;
    }
}
...
SomeWrapper wrapper = new SomeWrapper(); // note only 1 of these
using(var iter = categories.GetEnumerator()) {
    while(iter.MoveNext()) {
        wrapper.category = iter.Current;
        query = query.Elements("Category")
             .Where(wrapper.AnonMethod);
    }
}

      

This may not be obvious, but since Where

it is not evaluated immediately, the value category

(via the predicate AnonMethod

) is not validated until much later. This is an unfortunate consequence of the exact details of the C # specification. The view tmp

( inside the foreach) means that the capture happens per iteration:

class SecondWrapper {
    public string tmp;
    public bool AnonMethod(XElement c) {
        return c.Attribute("Name").Value == tmp;
    }
}
...
string category;
using(var iter = categories.GetEnumerator()) {
    while(iter.MoveNext()) {
        category = iter.Current;
        SecondWrapper wrapper = new SecondWrapper(); // note 1 per iteration
        wrapper.tmp = category;
        query = query.Elements("Category")
             .Where(wrapper.AnonMethod);
    }
}

      

And therefore, it doesn't matter if we evaluate now or later. Difficult and messy. You can see why I prefer changing the spec !!!

+3


a source


Here's a code similar to 2nd Mark's example, but tried and tested.

var q = from t in doc.Root.Elements("Category")
        where t.Attribute("Name").Value == "Tasties"
        from p in t.Elements("Category")
        where p.Attribute("Name").Value == "Pasta"
        from c in p.Elements("Category")
        where c.Attribute("Name").Value == "Chicken"
        from r in c.Elements("Recipe")
        select r.Attribute("Name").Value;

foreach (string recipe in q)
{
    Console.WriteLine("Recipe name = {0}", recipe);
}

      



In general, I would say that you only need one operator select

in your LINQ queries. You were getting IEnumerable<IEnumerable<String>>

because of your nested select statements.

+1


a source


If you add a using statement for System.Xml.XPath, this will add an XPathSelectElements () extension method to your XDocument. This will allow you to select nodes using the XPath operator if you're more comfortable with that.

Otherwise, you can flatten your IEnumerable <

IEnumerable <

String >>

only on the IEnumerable string <

>

using SelectMany:

IEnumerable<IEnumerable<String>> foo = myLinqResults;
IEnumerable<string> bar = foo.SelectMany(x => x);

      

+1


a source


A bit late, but extension methods can really help clean up messy LINQ to XML queries. For your scenario, you can work with code like this:

var query = xml.Root
               .Category("Tasties")
               .Category("Pasta")
               .Category("Chicken")
               .Recipes();

      

... using some of the techniques I show in From LINQ To XPath and Back Again

+1


a source







All Articles