Transforming Sequences
These methods turn one sequence into another shape: running totals, sliding windows, runs of equal keys, every combination of two lists, a tree built from a flat table, or one async call per item with a limit on how many run at once. Reach for them when plain LINQ would need a hand-written loop with state.
At a glance
| Method | What it does |
|---|---|
Iterate | Endless sequence: a seed, then a function applied again and again. |
Scan | Every intermediate result of an accumulation: [1, 2, 3, 4] → [1, 3, 6, 10]. |
Window | Sliding windows of a fixed size: [1, 2, 3, 4] → [[1, 2, 3], [2, 3, 4]]. |
DistinctUntilChanged | Drops items equal to the one right before them: [1, 1, 2, 1] → [1, 2, 1]. |
GroupAdjacent | Groups runs of consecutive items with the same key, keeping order. |
CartesianProduct | Pairs every item of one sequence with every item of another. |
Permutations | Every ordering of the items. |
Combinations | Every subset of exactly size items. |
PowerSet | Every subset, from empty to full. |
RecursiveJoin | Builds a tree from a flat list of items that point to their parent. |
RecursiveSelect | Flattens a tree depth-first, optionally with index and depth. |
SelectAsync | Runs an async operation per item with at most N at once; results keep input order. |
WithProgress | Pairs each item with an EnumerationProgress snapshot for progress bars. |
EnumerationProgress | Position, elapsed time and estimated time left of a loop. |
Fraction | Share of a loop finished before the current item, from 0 to just below 1. |
All of these live in the System.Collections.Generic namespace (class EnumerableExtensions, and the struct
EnumerationProgress), so a file that
already uses List<T> needs no extra using. The examples also use LINQ (Select, ToList), which needs
using System.Linq;.
Most methods here are lazy and stream the source one item at a time. A few read the whole source first:
| Method | Reads the source |
|---|---|
Iterate, Scan, Window, DistinctUntilChanged, GroupAdjacent, RecursiveSelect | Lazily, one item at a time. |
CartesianProduct | source lazily; other once, into a list, when enumeration starts. |
Permutations, Combinations | Into an array when enumeration starts. |
PowerSet, RecursiveJoin, SelectAsync | Into memory when you call the method. |
WithProgress | Into a list when enumeration starts, unless it already is an IReadOnlyList<T> (arrays, List<T>). |
Argument errors (a null source or delegate, an out-of-range size) are thrown when you call the method, not when you
first enumerate the result.
Running values
Iterate
Iterate produces an endless sequence: the seed, then next(seed), then next(next(seed)), and so on. Use it for
schedules, back-off steps or any series where each value comes from the previous one. Always finish it with Take
or TakeWhile.
IEnumerable<int> backoffSeconds = 1.Iterate(seconds => seconds * 2).Take(5);
// → [1, 2, 4, 8, 16]
DateTime firstBilling = new DateTime(2024, 1, 31);
DateTime contractEnd = new DateTime(2024, 5, 1);
List<DateTime> billingDates = firstBilling
.Iterate(date => date.AddMonths(1))
.TakeWhile(date => date <= contractEnd)
.ToList();
// → [2024-01-31, 2024-02-29, 2024-03-29, 2024-04-29]
next is called only when the next item is requested, so Take(1) never calls it. A null next throws
ArgumentNullException. The seed itself may be any value, including null.
Each step works on the previous result, not on the seed. In the example above AddMonths clamps January 31 to
February 29, and every later date stays on the 29th. If you need "the last day of every month", compute each date
from the seed instead, for example Enumerable.Range(0, 4).Select(i => firstBilling.AddMonths(i)).
Scan
Scan is Aggregate that returns every intermediate result instead of only the last one: running totals, running
maximums, account balances after each payment, breadcrumb paths.
| Overload | Returns |
|---|---|
Scan(accumulator) | The first item, then each accumulated value. The item type stays the same. |
Scan(seed, accumulator) | One accumulated value per item. The seed itself is not returned, and the result type may differ from the item type. |
int[] dailyOrders = { 3, 1, 4, 1, 5 };
IEnumerable<int> peakSoFar = dailyOrders.Scan(Math.Max);
// → [3, 3, 4, 4, 5]
string[] segments = { "docs", "api", "v2" };
IEnumerable<string> breadcrumbs = segments.Scan((path, segment) => $"{path}/{segment}");
// → ["docs", "docs/api", "docs/api/v2"]
decimal[] payments = { 120m, -20m, 45.5m };
IEnumerable<decimal> balance = payments.Scan(0m, (total, amount) => total + amount);
// → [120m, 100m, 145.5m]
An empty source yields nothing with either overload. With a single item, Scan(accumulator) yields that item and
never calls the accumulator. A null source or accumulator throws ArgumentNullException.
Window
Window(size) slides a window of size items over the sequence, moving one item at a time. Use it for moving
averages, "three failed logins in a row" checks, or comparing each item with its neighbours.
double[] dailyRevenue = { 2.0, 4.0, 6.0, 8.0 };
IEnumerable<double> movingAverage = dailyRevenue.Window(2).Select(window => window.Average());
// → [3.0, 5.0, 7.0]
List<IReadOnlyList<int>> windows = new[] { 1, 2, 3, 4, 5 }.Window(3).ToList();
// → [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
Only full windows are returned, so a sequence shorter than size yields nothing, and a sequence of exactly size
items yields one window. Each window is a fresh copy, so it is safe to keep windows after the loop moves on.
size below 1 throws ArgumentOutOfRangeException; a null source throws ArgumentNullException.
Window overlaps. For non-overlapping chunks of a fixed size, use Chunk (.NET 6+) or, on an array, this library's Split(size).
For pairs of neighbours with a selector, use Pairwise. Both are listed in the API reference.
Change detection
DistinctUntilChanged
DistinctUntilChanged drops an item when it equals the item right before it. Unlike Distinct, a value that comes
back later is kept. Use it for status feeds, sensor readings or UI state where only changes matter.
| Overload | Compares |
|---|---|
DistinctUntilChanged() | The items, with default equality. |
DistinctUntilChanged(keySelector, comparer = null) | The key of each item, with comparer, or default equality when it is null. |
int[] readings = { 1, 1, 2, 2, 2, 1 };
IEnumerable<int> changes = readings.DistinctUntilChanged();
// → [1, 2, 1]
(int Id, string Status)[] orderEvents =
{
(1, "Open"), (2, "Open"), (3, "Closed"), (4, "Open"),
};
IEnumerable<int> transitionIds = orderEvents
.DistinctUntilChanged(orderEvent => orderEvent.Status)
.Select(orderEvent => orderEvent.Id);
// → [1, 3, 4]
string[] regions = { "eu", "EU", "us", "US", "eu" };
IEnumerable<string> regionChanges = regions.DistinctUntilChanged(region => region, StringComparer.OrdinalIgnoreCase);
// → ["eu", "us", "eu"]
The first item is always returned, even when it equals default (for example 0). null items and keys are
compared safely: consecutive nulls collapse into one. A null source or key selector throws
ArgumentNullException.
GroupAdjacent
GroupAdjacent(keySelector) groups runs of consecutive items that share a key. Unlike GroupBy, a key that comes
back later starts a new group, and the original order is kept. Use it to split a log into days, compress runs of
the same log level, or group sorted rows in one pass.
Each group is a tuple (TKey Key, IReadOnlyList<T> Items).
string[] logLines = { "INFO start", "INFO load", "WARN slow", "INFO done" };
IEnumerable<string> summary = logLines
.GroupAdjacent(line => line.Split(' ')[0])
.Select(run => $"{run.Key} x{run.Items.Count}");
// → ["INFO x2", "WARN x1", "INFO x1"]
foreach ((string level, IReadOnlyList<string> lines) in logLines.GroupAdjacent(line => line.Split(' ')[0]))
{
// First pass: level → "INFO", lines → ["INFO start", "INFO load"]
}
Keys are compared with default equality; there is no comparer overload. null keys are grouped together like any
other key. An empty source yields no groups. A null source or key selector throws ArgumentNullException.
Combinatorics
CartesianProduct
CartesianProduct pairs every item of the source with every item of other, in order: the first source item with
every other item, then the second, and so on. Use it to build product variants, test matrices or price grids.
| Overload | Returns |
|---|---|
CartesianProduct(other) | Tuples (T1 First, T2 Second). |
CartesianProduct(other, resultSelector) | One resultSelector(first, second) per pair. |
string[] sizes = { "S", "M" };
string[] colors = { "Red", "Blue" };
IEnumerable<(string First, string Second)> pairs = sizes.CartesianProduct(colors);
// → [("S", "Red"), ("S", "Blue"), ("M", "Red"), ("M", "Blue")]
IEnumerable<string> skus = sizes.CartesianProduct(colors, (size, color) => $"{size}-{color}");
// → ["S-Red", "S-Blue", "M-Red", "M-Blue"]
other is enumerated once per enumeration of the result and buffered, so an expensive query is not re-run for every
source item. If either sequence
is empty, the result is empty. Any null argument throws ArgumentNullException. For three or more sequences,
chain the calls with a result selector.
Permutations
Permutations() yields every ordering of the items, in lexicographic order of their positions: the source order
first, its reverse last. Use it for small brute-force searches, such as trying every delivery route.
string[] warehouses = { "NYC", "BOS", "PHL" };
IEnumerable<string> routes = warehouses.Permutations().Select(route => string.Join(" > ", route));
// → ["NYC > BOS > PHL", "NYC > PHL > BOS", "BOS > NYC > PHL", "BOS > PHL > NYC", "PHL > NYC > BOS", "PHL > BOS > NYC"]
Each permutation is a fresh IReadOnlyList<T>. An empty source yields one empty permutation; a single item yields
one permutation. Items are permuted by position, so equal items produce repeated permutations:
new[] { "x", "x" }.Permutations() yields ["x", "x"] twice. A null source throws ArgumentNullException.
There are n! permutations: 10 items already give 3,628,800. The result is lazy, so Take, First or Where stop
early, but never call ToList() on the permutations of a large input.
Combinations
Combinations(size) yields every subset of exactly size items. Inside each subset the source order is kept, and
the subsets come in ascending order of positions.
string[] reviewers = { "ana", "ben", "cho" };
List<IReadOnlyList<string>> pairs = reviewers.Combinations(2).ToList();
// → [["ana", "ben"], ["ana", "cho"], ["ben", "cho"]]
int fiveChooseThree = Enumerable.Range(1, 5).Combinations(3).Count();
// → 10
size | Result |
|---|---|
0 | One empty subset. |
| Equal to the item count | The whole sequence, once. |
| Greater than the item count | Nothing. |
| Negative | ArgumentOutOfRangeException (parameter size). |
A null source throws ArgumentNullException.
PowerSet
PowerSet() yields every subset of the items, from the empty set to the full set, in increasing bitmask order:
subset i contains item k when bit k of i is set. Use it to test every on/off mix of a few feature flags.
string[] flags = { "dark-mode", "beta-search" };
List<IReadOnlyList<string>> mixes = flags.PowerSet().ToList();
// → [[], ["dark-mode"], ["beta-search"], ["dark-mode", "beta-search"]]
int subsetCount = Enumerable.Range(1, 10).PowerSet().Count();
// → 1024
The items are read once, when you call PowerSet. There may be at most 30 of them (2^30 subsets); a longer source
throws ArgumentException (parameter source) at call time. An empty source yields one empty subset. A null
source throws ArgumentNullException.
Trees
RecursiveJoin
RecursiveJoin builds a tree from a flat list in which each item points to its parent, such as a categories table
with Id and ParentId columns. It returns the root nodes; each node is whatever your result selector creates.
The two key selectors are easy to mix up:
| Parameter | Returns |
|---|---|
parentKeySelector | The item's own key, the one its children point to (category => category.Id). |
childKeySelector | The key of the item's parent (category => category.ParentId). |
An item is a root when its parent key matches no item's own key. A null parent key is fine and is the usual way
to mark roots.
public sealed record Category(int Id, int? ParentId, string Name);
public sealed record CategoryNode(string Name, int Depth, IReadOnlyList<CategoryNode> Children);
Category[] categories =
{
new Category(1, null, "Electronics"),
new Category(2, 1, "Phones"),
new Category(3, 1, "Laptops"),
new Category(4, 2, "Accessories"),
new Category(5, null, "Books"),
};
List<CategoryNode> tree = categories
.RecursiveJoin(
category => category.Id,
category => category.ParentId,
(Category category, int index, int depth, IEnumerable<CategoryNode> children) =>
new CategoryNode(category.Name, depth, children.ToList()))
.ToList();
IEnumerable<string> rootNames = tree.Select(node => node.Name);
// → ["Electronics", "Books"]
IEnumerable<string> electronics = tree[0].Children.Select(node => node.Name);
// → ["Phones", "Laptops"]
int accessoriesDepth = tree[0].Children[0].Children[0].Depth;
// → 2
Here TKey is inferred as int?, because Id is int and ParentId is int?. Pass the tree to
ToTreeString(node => node.Children, node => node.Name) (see Console output) to print it:
Electronics
├── Phones
│ └── Accessories
└── Laptops
Books
The result selector comes in three shapes, each also available with a trailing IComparer<TKey>? comparer
(six overloads in total):
| Result selector | Receives |
|---|---|
(item, children) | The item and its child nodes. |
(item, index, children) | Also the item's index among its siblings (roots are numbered among roots). |
(item, index, depth, children) | Also its depth: roots are 0, their children 1, and so on. |
public sealed record Department(string Code, string? ParentCode, string Name);
Department[] departments =
{
new Department("SALES", null, "Sales"),
new Department("EMEA", "sales", "Sales EMEA"),
};
List<string> roots = departments
.RecursiveJoin(
department => department.Code,
department => department.ParentCode,
(Department department, IEnumerable<string> children) => department.Name,
StringComparer.OrdinalIgnoreCase)
.ToList();
// → ["Sales"]
With the default comparer (or null), "sales" does not match "SALES", so both departments would be roots.
The compiler cannot infer TResult from an implicitly typed lambda here, because TResult also appears in the
type of the children parameter. (category, children) => new CategoryNode(...) fails with CS0411. Write the
parameter types, as in the examples above, or pass the type arguments:
categories.RecursiveJoin<Category, int?, CategoryNode>(...).
Notes:
- The source is read once, when you call
RecursiveJoin, and the key selectors run then (and again while the tree is enumerated). Building the lookup is O(n log n). Keys are compared through anIComparer<TKey>, so the key type must be comparable (int,string,Guid, ...) or you must pass a comparer. - Roots and children keep their source order.
childrenis lazy: the result selector of each child runs every timechildrenis enumerated. CallToList()inside the selector, as above, so each node is built once.- Items that only reach each other through a cycle (A's parent is B, B's parent is A) are never roots, so they never appear in the result.
- An empty source returns no roots. A
nullsource or selector throwsArgumentNullException.
RecursiveSelect
RecursiveSelect is the opposite of RecursiveJoin: it flattens a tree depth-first, so each item is followed by all
its descendants before its next sibling. Use it to search a menu, collect every file in a folder tree, or render an
indented outline.
| Overload | Projects |
|---|---|
RecursiveSelect(childSelector) | Nothing; returns the items themselves. |
RecursiveSelect(childSelector, item => ...) | Each item. |
RecursiveSelect(childSelector, (item, index) => ...) | Each item with its index among its siblings. |
RecursiveSelect(childSelector, (item, index, depth) => ...) | Also the depth; top-level items are 0. |
public sealed record MenuItem(string Title, IReadOnlyList<MenuItem>? Children = null);
MenuItem[] menu =
{
new MenuItem("Settings", new[]
{
new MenuItem("Account", new[] { new MenuItem("Password") }),
new MenuItem("Billing"),
}),
new MenuItem("Help"),
};
IEnumerable<string> titles = menu.RecursiveSelect(item => item.Children).Select(item => item.Title);
// → ["Settings", "Account", "Password", "Billing", "Help"]
IEnumerable<string> positions = menu.RecursiveSelect(
item => item.Children,
(item, index) => $"{item.Title}:{index}");
// → ["Settings:0", "Account:0", "Password:0", "Billing:1", "Help:1"]
IEnumerable<string> outline = menu.RecursiveSelect(
item => item.Children,
(item, index, depth) => $"{new string(' ', depth * 2)}{item.Title}");
Settings
Account
Password
Billing
Help
A null from childSelector means "no children". The walk is lazy: children are requested only when the walk
reaches them. An empty source yields nothing; a null source or delegate throws ArgumentNullException.
RecursiveSelect does not detect cycles. If a node is its own descendant, the walk keeps going deeper until the
process crashes with a stack overflow (unless you stop early with Take). A very deep tree can overflow the same way.
Async and progress
SelectAsync
SelectAsync runs an async operation for every item with at most maxConcurrency operations running at once, and
returns the results as an array in the original order. It is the usual way to call an API for 1,000 items without
opening 1,000 connections.
| Overload | Use when |
|---|---|
SelectAsync(selector, maxConcurrency) | The operation takes only the item. |
SelectAsync((item, token) => ..., maxConcurrency, cancellationToken) | The operation should stop on cancellation or when another item fails. |
string[] invoiceIds = { "INV-1001", "INV-1002", "INV-1003" };
byte[][] pdfs = await invoiceIds.SelectAsync(
(invoiceId, token) => invoiceClient.DownloadPdfAsync(invoiceId, token),
maxConcurrency: 4,
cancellationToken);
// pdfs[0] is the PDF for "INV-1001", whichever download finished first
int[] delaysInMilliseconds = { 30, 1, 20, 5, 10 };
int[] results = await delaysInMilliseconds.SelectAsync(
async delay =>
{
await Task.Delay(delay);
return delay;
},
maxConcurrency: 3);
// → [30, 1, 20, 5, 10]
How it behaves:
- The source is enumerated once, when you call the method. Exactly
maxConcurrencyoperations start at once (fewer if there are fewer items), and each one that finishes makes room for the next item. - After the first failure no new operations start, the running ones get a cancelled token, and the original
exception is rethrown (not an
OperationCanceledException). - When
cancellationTokenis cancelled, no new operations start, running ones see the cancellation through their token, and the returned task fails withOperationCanceledException. - An empty source returns an empty array without calling the selector.
- A
nullsource or selector throwsArgumentNullException;maxConcurrencybelow 1 throwsArgumentOutOfRangeException. Both are thrown when you call the method, before you await.
SelectAsync does not start threads. It calls your selector and awaits the tasks it returns, so it is meant for
I/O-bound work: HTTP calls, database queries, file reads. For CPU-bound work, use Parallel.ForEachAsync, or
Task.Run inside the selector.
See Tasks for timeouts, retries and other helpers you can combine with the selector.
WithProgress
WithProgress() pairs every item with an EnumerationProgress snapshot: where the loop is,
how long it has run, and how long it will probably still take. Use it for progress bars and status lines in
command-line tools and batch jobs.
string[] invoiceFiles = Directory.GetFiles("invoices", "*.pdf");
foreach ((string file, EnumerationProgress progress) in invoiceFiles.WithProgress())
{
Console.Write($"\r{progress} ");
ArchiveInvoice(file);
}
4/10 (30%), 1.2s elapsed, ~2.8s left
- The elapsed time is measured when each item is handed out, so the estimate reflects how long the loop body took for the previous items.
- If the source is not already an
IReadOnlyList<T>(arrays andList<T>are), it is copied into a list when the loop starts, so the total is known. The clock starts after that copy. - An empty source yields nothing. A
nullsource throwsArgumentNullExceptionwhen you callWithProgress.
EnumerationProgress
EnumerationProgress is a read-only struct that describes one position in a loop. WithProgress creates it for
you; you can also create one yourself with new EnumerationProgress(index, total, elapsed), for example in tests.
| Member | Meaning |
|---|---|
Index | Zero-based position of the current item. |
Total | Total number of items. |
Elapsed | Time since the loop started, measured when the current item was handed out. |
Completed | Items handed out before the current one (same as Index). |
Remaining | Items still to come after the current one. |
Fraction | Completed share: 0 for the first item, just below 1 for the last. |
Percent | Fraction × 100. |
IsLast | true for the last item. |
EstimatedRemaining | Time left, assuming the remaining items take as long on average as the completed ones; null for the first item. |
ToString() | A status line, formatted with the invariant culture. |
Equals, ==, !=, GetHashCode | Value equality on Index, Total and Elapsed. |
EnumerationProgress progress = new EnumerationProgress(4, 10, TimeSpan.FromSeconds(2));
int completed = progress.Completed; // → 4
int remaining = progress.Remaining; // → 5
double percent = progress.Percent; // → 40.0
bool isLast = progress.IsLast; // → false
TimeSpan? timeLeft = progress.EstimatedRemaining; // → TimeSpan.FromSeconds(3)
string statusLine = progress.ToString(); // → "5/10 (40%), 2.0s elapsed, ~3.0s left"
EnumerationProgress first = new EnumerationProgress(0, 10, TimeSpan.FromMilliseconds(500));
string firstLine = first.ToString(); // → "1/10 (0%), 500ms elapsed"
TimeSpan? noEstimate = first.EstimatedRemaining; // → null
ToString() shows times under a second as milliseconds (500ms), under a minute with one decimal (1.2s), and
longer ones as minutes and seconds (5m 00s). The "left" part is missing for the first item, because there is
nothing to extrapolate from yet.
Percent is the share finished before the current item, so it never reaches 100 inside the loop: for 10 items
the last one shows 90%. Print a final "done" line after the loop if you want to show 100%.
The constructor throws ArgumentOutOfRangeException when total is negative (parameter total), or when index is
negative or not below total (parameter index).
Fraction
Fraction is the share of the loop finished before the current item, as a double from 0 to just below 1. It
is Index / Total. Use it when a progress bar or UI control wants a ratio rather than a percentage; use Percent
when you want 0 to 100.
EnumerationProgress progress = new EnumerationProgress(4, 10, TimeSpan.FromSeconds(2));
double fraction = progress.Fraction; // → 0.4
int filled = (int)(fraction * 20); // → 8
string bar = new string('#', filled).PadRight(20, '.'); // → "########............"
EnumerationProgress first = new EnumerationProgress(0, 3, TimeSpan.Zero);
double atStart = first.Fraction; // → 0.0
EnumerationProgress second = new EnumerationProgress(1, 3, TimeSpan.FromSeconds(1));
double afterOne = second.Fraction; // → 0.3333333333333333
In a loop, read it from the snapshot that WithProgress hands you:
IProgress<double> importProgress = new Progress<double>(ratio => progressBar.Value = ratio);
List<Order> pendingOrders = orderRepository.GetPending();
foreach ((Order order, EnumerationProgress progress) in pendingOrders.WithProgress())
{
importProgress.Report(progress.Fraction);
ImportOrder(order);
}
Fractionnever reaches1inside the loop: for 10 items the last one gives0.9. Report1yourself after the loop ends.- A snapshot made by the constructor or by
WithProgressalways hasTotalof at least1, because the constructor rejects anindexthat is not belowtotalandWithProgressyields nothing for an empty source. SoFractionis neverNaNthere.
default(EnumerationProgress) skips the constructor and has Total 0, so its Fraction is NaN (0.0 / 0).
Do not use a default snapshot as a placeholder for "no progress yet".
See also
- Querying sequences for filtering, partitioning and other LINQ additions.
- Statistics for medians, percentiles and other numbers over sequences.
- Console output for printing trees and tables, and for
ToCsv. - Tasks for timeouts, retries and awaiting several tasks at once.