Рекурсивные вызовы LINQ
Я пытаюсь построить дерево XML некоторых данных с родительским отношением дочерних элементов, но в той же таблице.
Двумя важными областями являются
CompetitionID
ParentCompetitionID
Некоторые данные могут быть
CompetitionID = 1,
ParentCompetitionID = нуль
CompetitionID = 2,
ParentCompetitionID = 1
CompetitionID = 3,
ParentCompetitionID = 1
Сломанный запрос, который я просто показываю, отображает результаты в плоском формате. Видя, что я работаю с XML, требуется какая-то рекурсивная функциональность. Я могу сделать это, используя обычную для рекурсии цикла, но хотел бы увидеть версию linq. Любая помощь была оценена.
var results =
from c1 in comps
select new {
c.CompetitionID,
SubComps=
from sc in comps.Where (c2 => c2.CompetitionID == c1.CompetitionID)
select sc
};
Update
Я нашел интересную статью Chris Eargle здесь, в которой показано, как рекурсивно вызывать лямбда-делегатов. Вот код. Спасибо Крису!
Func<int, int> factoral = x => x <= 1 ? 1 : x + factoral(--x);
Func<int, int> factoral = null;
factoral = x => x <= 1 ? 1 : x + factoral(--x);
^ добавлено форматирование кода, чтобы показать функции lamba
Хитрость заключается в том, чтобы сначала присвоить делегату Func нуль.
Ответы
Ответ 1
Не знаю, как написать рекурсивный LINQ. Но я думаю, что здесь не требуется никакой рекурсии. Дерево можно построить всего за два шага:
Dictionary<int, Competition> dic = comps.ToDictionary(e => e.CompetitionID);
foreach (var c in comps)
if (dic.ContainsKey(c.ParentCompetitionID))
dic[c.ParentCompetitionID].Children.Add(c);
var root = dic[1];
Корневая переменная теперь содержит полное дерево.
Здесь полный образец для тестирования:
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApplication2
{
class Competition
{
public int CompetitionID;
public int ParentCompetitionID;
public List<Competition> Children=new List<Competition>();
public Competition(int id, int parent_id)
{
CompetitionID = id;
ParentCompetitionID = parent_id;
}
}
class Program
{
static void Main(string[] args)
{
List<Competition> comps = new List<Competition>()
{
new Competition(1, 0),
new Competition(2,1),
new Competition(3,1),
new Competition(4,2),
new Competition(5,3)
};
Dictionary<int, Competition> dic = comps.ToDictionary(e => e.CompetitionID);
foreach (var c in comps)
if (dic.ContainsKey(c.ParentCompetitionID))
dic[c.ParentCompetitionID].Children.Add(c);
var root = dic[1];
}
}
}
Ответ 2
Я знаю, что я слишком поздно здесь. Но вы сказали, что у вас уже есть версия, использующая foreach:) Итак, если она действительно должна быть рекурсивной и использовать linq, это будет решением:
internal class Competition
{
public int CompetitionID;
public int ParentCompetitionID;
public Competition(int id, int parentId)
{
CompetitionID = id;
ParentCompetitionID = parentId;
}
}
internal class Node
{
public Node(int id, IEnumerable<Node> children)
{
Children = children;
Id = id;
}
public IEnumerable<Node> Children { get; private set; }
public int Id { get; private set; }
}
internal class Program
{
static void Main(string[] args)
{
var comps = new List<Competition>
{
new Competition(1, 0),
new Competition(2, 1),
new Competition(3, 1),
new Competition(4, 2),
new Competition(5, 3)
};
Node root = ToTree(0, comps);
}
static readonly Func<int, IEnumerable<Competition>, Node> ToTree =
(nodeId, competitions) => new Node(nodeId, from c in competitions where c.ParentCompetitionID == nodeId select ToTree(c.CompetitionID, competitions));
}
Ответ 3
Вы можете получить древовидную структуру, объединив LINQ и рекурсию с делегатами. В этом примере я использую XML-структуру следующим образом:
<Competitions>
<Competition ID="1" />
<Competition ID="2" ParentCompetitionID="1" />
<Competition ID="3" ParentCompetitionID="1" />
<Competition ID="4" />
</Competitions>
Итак, чтобы хранить node данные в коде и упростить навигацию, создайте класс следующим образом:
class Competition
{
public int CompetitionID { get; set; }
public IEnumerable<Competition> Childs { get; set; }
}
Теперь, используя Linq to XML, вы загружаете xml файл в XDocument. После этого объявите делегата, который выполняет итерацию по всем элементам xml внутри документа, выбирая узлы, которые имеют идентификатор, определяющий идентификатор id делегата. При выборе каждого node он снова вызывает делегата, передавая идентификатор родительского node для поиска. Сначала он начинается с того, что параметр id установлен в значение null, поэтому, выбрав firts корневые узлы:
var doc = XDocument.Load("tree.xml");
//Declare the delegate for using it recursively
Func<int?, IEnumerable<Competition>> selectCompetitions = null;
selectCompetitions = (int? id) =>
{
return doc.Elements("Competitions").Elements().Where(c =>
{
//If id is null return only root nodes (without ParentCompetitionID attribute)
if (id == null)
return c.Attribute("ParentCompetitionID") == null;
else
//If id has value, look for nodes with that parent id
return c.Attribute("ParentCompetitionID") != null &&
c.Attribute("ParentCompetitionID").Value == id.Value.ToString();
}).Select(x => new Competition()
{
CompetitionID = Convert.ToInt32(x.Attribute("ID").Value),
//Always look for childs with this node id, call again to this
//delegate with the corresponding ID
Childs = selectCompetitions(Convert.ToInt32(x.Attribute("ID").Value))
});
};
var competitions = selectCompetitions(null);
Чтобы протестировать его, вы можете сделать простой повторяющийся метод, который выводит дерево на консоль:
private static void Write(IEnumerable<Competition> competitions, int indent)
{
foreach (var c in competitions)
{
string line = String.Empty;
for (int i = 0; i < indent; i++)
{
line += "\t";
}
line += "CompetitionID = " + c.CompetitionID.ToString();
Console.WriteLine(line);
if (c.Childs != null && c.Childs.Count() > 0)
{
int id = indent + 1;
Write(c.Childs, id);
}
}
}
Надеюсь, что это поможет!
Ответ 4
Я сделал что-то очень похожее, используя LINQ group by
Я не использую синтаксис запроса LINQ, так что простите меня, если это не так:
var results = from c in comps
group c by c.ParentCompetitionID into g
select new { ParentId = g.Key, ChildId = g };
Конечно, это было бы лучше, если бы ваши классы выглядели примерно так:
class Competition {
int Id;
string Description;
Competition ParentCompetition;
}
Затем вместо группировки по идентификатору вы можете группироваться по всему конкурсу, что делает процесс генерации XML более быстрым и легким.
var results = from c in comps
group c by c.ParentCompetition into g
select new { Parent = g.Key, Child = g };
Ответ 5
class Competition
{
int ID { get; set;}
int ParentID { get; set; }
IEnumerable<Competition> Children { get; set; }
}
public IEnumerable<Competition> GetChildren(
IEnumerable<Competition> competitions, int parentID)
{
IEnumerable<Competition> children =
competitions.Where(c => c.ParentID == parentID);
if (children.Count() == 0)
return null;
return children.Select(
c => new Competition { ID = c.ID, Children = GetChildren(c.ID) };
}
Затем вы можете просто вызвать GetChildren, передав идентификатор корня в качестве родительского идентификатора, и он вернет древовидную структуру. Вы также можете изменить объект Competition
на XML-API по вашему выбору.
Я знаю, что это не совсем то, что вы ищете, но afaik LINQ не поддерживает рекурсию. Тем не менее LIN LINK означает интегрированный язык, что я и использовал.
Ответ 6
Пока вы не можете сделать это с помощью одного запроса (если вы не вызываете SQL напрямую с CTE), вы можете ограничить количество запросов глубиной дерева.
Слишком длинный код для вставки, но основными шагами являются:
- Соберите корневые узлы и добавьте в "все" узлы
- Соберите узлы с родителями во всех "узлах" (список пересылки для запроса)
- Добавить узлы на шаге 2 на все узлы
- Повторяйте с 2 по 3 до 2-го шага, возвращая 0 узлов (что, я думаю, должно быть глубина дерева + 1).
Вы можете свести к минимуму количество узлов, переданных запросу, на шаге 2. SQL-сервер имеет тенденцию бомбить со списком более 2000 записей. (SQL Compact не имеет такой проблемы, хотя).