← All articles

From flat list to tree in one line of C#

A quick look at turning a flat collection with parent references into a navigable hierarchy, without writing recursion yourself.
From flat list to tree in one line of C#

Almost every application grows a hierarchy at some point: categories with subcategories, org charts, folder structures, menus. The data usually lives in a flat table with a ParentId column, and sooner or later you find yourself writing the same recursive loop to stitch it back together — and the same defensive check against a row that accidentally points at its own descendant.

That loop is worth extracting once and never writing again. With the Regira.TreeList package (Apache-2.0) it's an extension method:

using Regira.TreeList;

class Person { public int Id { get; set; } public string Name { get; set; } = ""; public int? ParentId { get; set; } }

var people = new[]
{
    new Person { Id = 1, Name = "Alice", ParentId = null },
    new Person { Id = 2, Name = "Bob",   ParentId = 1 },
    new Person { Id = 3, Name = "Carol", ParentId = 1 },
};

var tree = people.ToTreeList(p => people.FirstOrDefault(x => x.Id == p.ParentId));

Console.WriteLine(tree.Roots.Length);            // 1  (Alice)
Console.WriteLine(tree.Roots[0].Children.Count); // 2  (Bob, Carol)

ToTreeList wraps every item in a TreeNode<T> that knows its Value, Parent, Children and Level (0 for roots). The TreeList<T> itself inherits List<TreeNode<T>>, so the whole tree stays LINQ-friendly — it's a flat list of nodes and a hierarchy at the same time.

Three things you get for free:

Navigation. Nodes and node collections get extension methods for walking the tree in any direction — GetAncestors(), GetOffspring(), GetRoots(), GetBrothers() (siblings), on single nodes even GetUncles() and GetNephews():

var bob = tree.First(n => n.Value.Name == "Bob");
var ancestors = bob.GetAncestors();   // [Alice] — ordered root → parent
var leaves    = tree.GetBottom();     // all nodes without children

Depth-first ordering. tree.OrderByHierarchy() yields nodes parent-before-children — exactly the order you need to render an indented list or fill a <select> with -- prefixes. (There's also an overload that takes a key selector, but it sorts each root's entire subtree by that key, which breaks the parent-first order — if you want sorted siblings, sort the source rows before building the tree.)

Cycle protection. If a row ends up as its own ancestor — it happens, usually after a bulk import — the build still terminates. With the parent-selector overload the tree is grown downward from the roots, and rows caught in a cycle are never reachable from a root, so they're simply left out (compare tree.Count with your row count to detect them). In builds where a cycle member is reachable — the children-selector overload below, or manual AddChild calls — a typed InvalidChildException is thrown instead of looping forever, and you can opt into skipping instead:

var roots   = people.Where(p => p.ParentId == null);
var options = new TreeList<Person>.TreeOptions { ThrowOnError = false };
var tree = people.ToTreeList(roots,
    node => people.Where(p => p.ParentId == node.Value.Id),
    options); // invalid nodes are skipped instead of throwing

One performance note: the parent-selector overload is the convenient one, but that children-selector overload isn't just for options — when you already know the roots, it builds the tree top-down and is measurably faster on large sets:

var tree = people.ToTreeList(roots,
    node => people.Where(p => p.ParentId == node.Value.Id));

That's the whole trick: load flat, wrap once, navigate freely. Everything above is runnable as-is — grab the sample project (one Program.cs, one dotnet run).

Where this gets really interesting is when the hierarchy is too big to load into memory and you want the database to walk it — recursive CTEs mapped into EF Core, composing with your regular filters. That's a longer story, and the subject of an upcoming deep dive.

Docs and examples: regira.github.io/Regira-Packages/src/TreeList