Лучший способ сделать этот запрос LINQ to XML? - PullRequest
1 голос
/ 16 мая 2009

Скажем, у меня есть этот 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 ]

3 голосов
/ 16 мая 2009

Лично я бы использовал 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 (с "захваченным" итератором lvalue) выглядит так:

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 голос
/ 05 июня 2009

Немного поздно, но методы расширения могут действительно помочь убрать грязно выглядящие запросы LINQ to XML. Для вашего сценария вы можете работать с таким кодом:

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

... используя некоторые техники, которые я показываю в От LINQ до XPath и обратно

1 голос
/ 17 мая 2009

Если вы добавите оператор использования для System.Xml.XPath, это добавит метод расширения XPathSelectElements () в ваш XDocument. Это позволит вам выбирать узлы с помощью оператора XPath, если вам удобнее.

В противном случае вы можете сгладить свой IEnumerable < IEnumerable < String >> в одну строку IEnumerable < > с помощью SelectMany:

IEnumerable<IEnumerable<String>> foo = myLinqResults;
IEnumerable<string> bar = foo.SelectMany(x => x);
1 голос
/ 16 мая 2009

Вот код, который похож на второй пример Марка, но проверен и проверен.

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

В целом, я бы сказал, что вы хотите только один оператор select в ваших запросах LINQ. Вы получили IEnumerable<IEnumerable<String>> из-за ваших вложенных операторов выбора.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...