更好的方法来执行LINQ to XML查询?

时间:2009-05-16 18:45:56

标签: c# .net linq-to-xml

所以说我有这个XML文件:

<?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>

我想在Tasties \ Pasta \ Chicken中返回所有食谱的名称,我该怎么做?

目前我所拥有的是:

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);
    }
}

哪种方式有效,虽然它不检查路径,但仅适用于名为Chicken的第一类。我可以递归地挖掘路径中的每个元素,但似乎可能有一个我错过的更好的解决方案。当我想要的只是IEnumerable<IEnumerable<String>>时,我当前的查询会返回IEnumerable<String>

基本上我可以使它工作但它看起来很乱,我希望看到任何LINQ建议或技术来做更好的查询。

4 个答案:

答案 0 :(得分:3)

就个人而言,我会使用XmlDocument和熟悉的SelectNodes

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

对于LINQ-to-XML,我猜测(未经测试)类似于:

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);
}

编辑:如果您希望类别选择更灵活:

    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);
    }

这应该适用于任意数量的级别,选择所选级别或更低级别的所有食谱。


编辑以讨论Where为什么有本地tmp变量的讨论(评论):

这可能会有点复杂,但我正在尝试正义问题;-p

基本上,foreach(迭代器左值“捕获”)看起来像:

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);
    }
}

可能不是很明显,但由于Where未立即评估,category(通过谓词AnonMethod)的值直到很久才会被检查。这是C#规范精确细节的不幸后果。引入tmp作用于 foreach)意味着每次迭代都会发生捕获:

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);
    }
}

因此,我们现在或以后评估无关紧要。复杂而凌乱。你可以看到为什么我赞成改变规范!!!

答案 1 :(得分:1)

这里的代码类似于Marc的第二个例子,但经过测试和验证。

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);
}

一般来说,我会说你只想在LINQ查询中使用一个select语句。由于您的嵌套选择语句,您获得了IEnumerable<IEnumerable<String>>

答案 2 :(得分:1)

如果您为System.Xml.XPath add a using statement,则会向您的XDocument添加XPathSelectElements()扩展方法。如果您对此更加满意,那么这将允许您选择带有XPath语句的节点。

否则,您可以使用SelectMany将IEnumerable < IEnumerable <字符串>>展平为IEnumerable <字符串>

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

答案 3 :(得分:1)

有点晚了,但扩展方法确实可以帮助清理凌乱的LINQ to XML查询。对于您的场景,您可以使用以下代码:

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

...使用我在From LINQ To XPath And Back Again

中展示的一些技巧