C#
LINQ
Tree Traversal
Data Structures
Programming

Searching a tree using LINQ

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

LINQ is excellent for filtering, projecting, and aggregating sequences, but a tree is not a flat sequence until you make it one. The standard pattern is to traverse the tree with an iterator method, expose all nodes as IEnumerable<T>, and then let LINQ handle the search logic.

Model the Tree Clearly

Start with a node type that stores a value and its children:

csharp
1using System;
2using System.Collections.Generic;
3
4public sealed class Node
5{
6    public string Name { get; }
7    public List<Node> Children { get; } = new();
8
9    public Node(string name)
10    {
11        Name = name;
12    }
13}

LINQ itself will not recurse through Children automatically. You need to flatten the hierarchy first.

Flatten the Tree with an Iterator

The cleanest solution is an iterator method using yield return. This example performs a depth-first traversal and returns the current node plus all descendants.

csharp
1using System.Collections.Generic;
2
3public static class TreeExtensions
4{
5    public static IEnumerable<Node> DescendantsAndSelf(this Node root)
6    {
7        yield return root;
8
9        foreach (var child in root.Children)
10        {
11            foreach (var descendant in child.DescendantsAndSelf())
12            {
13                yield return descendant;
14            }
15        }
16    }
17}

Once the tree is exposed as IEnumerable<Node>, LINQ becomes natural.

Search with LINQ

Now you can query the tree as if it were any other sequence:

csharp
1using System;
2using System.Linq;
3
4var root = new Node("root");
5var settings = new Node("settings");
6var users = new Node("users");
7var profile = new Node("profile");
8
9root.Children.Add(settings);
10root.Children.Add(users);
11users.Children.Add(profile);
12
13Node? match = root
14    .DescendantsAndSelf()
15    .FirstOrDefault(node => node.Name == "profile");
16
17Console.WriteLine(match?.Name ?? "not found");

This is often the simplest answer to “search a tree with LINQ.” The traversal logic stays in one place, and the query itself remains short and expressive.

Search for Multiple Matches

If the tree may contain more than one match, switch from FirstOrDefault to Where:

csharp
1var matches = root
2    .DescendantsAndSelf()
3    .Where(node => node.Name.Contains("s"))
4    .Select(node => node.Name)
5    .ToList();
6
7foreach (var name in matches)
8{
9    Console.WriteLine(name);
10}

That is where LINQ shines. Once the sequence exists, filtering and projection are straightforward.

Depth-First Versus Breadth-First

The iterator above is depth-first. That is fine for many cases, but if you need the nearest matching node by level, breadth-first search may be a better fit.

Here is a breadth-first version:

csharp
1using System.Collections.Generic;
2
3public static class BreadthFirstTreeExtensions
4{
5    public static IEnumerable<Node> BreadthFirst(this Node root)
6    {
7        var queue = new Queue<Node>();
8        queue.Enqueue(root);
9
10        while (queue.Count > 0)
11        {
12            var current = queue.Dequeue();
13            yield return current;
14
15            foreach (var child in current.Children)
16            {
17                queue.Enqueue(child);
18            }
19        }
20    }
21}

Then query it the same way:

csharp
var firstByLevel = root
    .BreadthFirst()
    .FirstOrDefault(node => node.Name.StartsWith("p"));

Choose the traversal order based on search semantics, not style preference alone.

When a Recursive LINQ Expression Is Not Enough

Developers sometimes try to write a single nested LINQ expression that traverses the whole tree without a helper method. That usually becomes harder to read than the problem deserves.

The better pattern is:

  • one method that defines traversal
  • one LINQ query that defines the search

This separation keeps recursion and filtering concerns distinct.

Common Pitfalls

The biggest mistake is assuming LINQ can discover descendants automatically. A tree is hierarchical, so you must provide traversal logic first.

Another issue is using recursion on a very deep tree without considering stack depth. For exceptionally deep or untrusted input, an explicit stack or queue can be safer than recursive calls.

Developers also often forget that FirstOrDefault returns null for reference types when nothing matches. Always handle the not-found case cleanly.

Finally, do not overuse ToList() in the middle of the query unless you truly need materialization. LINQ works well as a streaming pipeline when the traversal iterator yields lazily.

Summary

  • LINQ can search a tree once you expose the nodes as IEnumerable<T>.
  • Use yield return to flatten the hierarchy cleanly.
  • 'FirstOrDefault works well for one match and Where for many matches.'
  • Pick depth-first or breadth-first traversal based on the search requirement.
  • Keep traversal logic separate from the query to keep the code readable.

Course illustration
Course illustration

All Rights Reserved.