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.
a source to share
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 !!!
a source to share
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.
a source to share
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);
a source to share
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
a source to share