Traverse is a .NET library that makes it easy to explore trees and tree-like structures using LINQ.
Traverse is available for download as a NuGet package.
The Traverse
class contains utility methods for lazily flattening trees and implicit trees into IEnumerable<T>
s.
For example, let's say we wanted to explore the implicit "tree" of an Exception
in search of OperationCanceledException
s. We could use Traverse
to express the algorithm like this:
Exception ex = ...;
var operationCanceledExceptions = Traverse.DepthFirst(
// the tree root
ex,
// a function which maps a tree "node" to its children
e => e is AggregateException a ? a.InnerExceptions : new[] { e.InnerException }.Where(ie => ie != null)
)
.OfType<OperationCanceledException>();
Without Traverse
, we might accomplish the same task by writing recursive code like this:
// without using Traverse
List<OperationCanceledException> FindOperationCanceledExceptions(Exception ex)
{
var result = new List<OperationCanceledException>();
void Search(Exception e)
{
if (e is OperationCanceledException oce) { result.Add(e); }
if (e is AggregateException a)
{
foreach (var ie in a.InnerExceptions) { Search(ie); }
}
else if (e.InnerException != null) { Search(e.InnerException); }
}
}
There are several reasons to use the Traverse
approach over hand-written recursion:
- More concise, especially because it can be defined inline
- Fully lazy traversal makes it easy to short-circuit (e. g. with
.First()
or.Take(n)
) - Can chain other LINQ methods for further processing because the traversal is abstracted behind
IEnumerable<T>
- Not vulnerable to
StackOverflowException
, because no actual recursion is happening - Method of traversal is explicit (e. g. DFS vs. BFS, preorder vs. postorder) and can be changed without a refactor
The library contains the following traversal methods:
- DepthFirst: Implements DFS. Pre-order by default; can be flipped to post-order by passing
postorder: true
. - BreadthFirst: Implements BFS. Can traverse from one root or from multiple roots.
- Along: Traverses along a singly-linked list.
For example, given the following directory structure:
C:\
C:\a
C:\a\b
C:\a\c
C:\d
C:\d\e
Here's how each API traverses:
// yields C:\, C:\a, C:\a\b, C:\a\c, C:\d, C:\d\e
var depthFirst = Traverse.DepthFirst(new DirectoryInfo(@"C:\"), d => d.GetDirectories());
// yields C:\a\b, C:\a\c, C:\a, C:\d\e, C:\d, C:\
var postorderDepthFirst =
Traverse.DepthFirst(new DirectoryInfo(@"C:\"), d => d.GetDirectories(), postorder: true);
// yields C:\, C:\a, C:\d, C:\a\b, C:\a\c, C:\d\e
var breadthFirst = Traverse.BreadthFirst(new DirectoryInfo(@"C:\"), d => d.GetDirectories());
// yields C:\a, C:\d, C:\a\b, C:\a\c, C:\d\e
var breadthFirstMultipleRoots = Traverse.BreadthFirst(
new[] { new DirectoryInfo(@"C:\a"), new DirectoryInfo(@"C:\d") },
d => d.GetDirectories()
);
// yields C:\a\c, C:\a, C:\
var along = Traverse.Along(new DirectoryInfo(@"C:\a\c"), d => d.Parent);
Sometimes, it can be useful to know your depth within the tree as you traverse. While this isn't built into the Traverse
library, it is easy enough to track:
Traverse.DepthFirst(
(dir: new DirectoryInfo(@"C:\"), depth: 0),
t => t.dir.GetDirectories().Select(d => (dir: d, depth: t.depth + 1))
);
The library assumes a tree structure, and makes no attempt to detect repeat visits to the same node (which you might see in a non-tree DAG) or cycles. However, this is also easily handled:
var visited = new HashSet<Node> { root };
// note, do not enumerate this sequence multiple times as the visited set will not reset!
var distinctTraversal = Traverse.BreadthFirst(root, n => n.Children.Where(visited.Add));
- 1.0.1 Fix nullable annotations on
Traverse.Along()
- 1.0.0 Initial release