BehavioralC#verifiedVerified
Iterator Pattern in C#
Provides a way to sequentially access elements of a collection without exposing its underlying representation.
How to Implement the Iterator Pattern in C#
1Step 1: Define a custom collection with iterator support
public class NumberRange : IEnumerable<int>
{
private readonly int _start;
private readonly int _end;
public NumberRange(int start, int end)
{
_start = start;
_end = end;
}2Step 2: Implement IEnumerable via yield return
public IEnumerator<int> GetEnumerator()
{
for (var i = _start; i <= _end; i++)
yield return i;
}
System.Collections.IEnumerator
System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}3Step 3: Tree node with depth-first iteration
public class TreeNode<T>(T value)
{
public T Value => value;
public List<TreeNode<T>> Children { get; } = [];
public IEnumerable<T> DepthFirst()
{
yield return Value;
foreach (var child in Children)
foreach (var item in child.DepthFirst())
yield return item;
}
}
// Usage:
// var range = new NumberRange(1, 5);
// foreach (var n in range) Console.Write(n + " "); // 1 2 3 4 5using System.Runtime.CompilerServices;
// [step] Define async paginated iterator for API consumption
public record Page<T>(
IReadOnlyList<T> Items, string? NextCursor, int TotalCount);
public interface IPagedDataSource<T>
{
Task<Page<T>> FetchPageAsync(
string? cursor, int pageSize,
CancellationToken ct = default);
}
// [step] Async enumerable that lazily fetches pages
public static class PaginatedIterator
{
public static async IAsyncEnumerable<T> CreateAsync<T>(
IPagedDataSource<T> source,
int pageSize = 50,
[EnumeratorCancellation] CancellationToken ct = default)
{
string? cursor = null;
do
{
var page = await source.FetchPageAsync(cursor, pageSize, ct);
foreach (var item in page.Items)
yield return item;
cursor = page.NextCursor;
}
while (cursor is not null);
}
}
// [step] Filtered and batched async enumerable extensions
public static class AsyncEnumerableExtensions
{
public static async IAsyncEnumerable<T> WhereAsync<T>(
this IAsyncEnumerable<T> source,
Func<T, bool> predicate,
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var item in source.WithCancellation(ct))
{
if (predicate(item))
yield return item;
}
}
public static async IAsyncEnumerable<IReadOnlyList<T>> BatchAsync<T>(
this IAsyncEnumerable<T> source,
int batchSize,
[EnumeratorCancellation] CancellationToken ct = default)
{
var batch = new List<T>(batchSize);
await foreach (var item in source.WithCancellation(ct))
{
batch.Add(item);
if (batch.Count >= batchSize)
{
yield return batch.AsReadOnly();
batch = new List<T>(batchSize);
}
}
if (batch.Count > 0)
yield return batch.AsReadOnly();
}
public static async Task<List<T>> ToListAsync<T>(
this IAsyncEnumerable<T> source,
CancellationToken ct = default)
{
var list = new List<T>();
await foreach (var item in source.WithCancellation(ct))
list.Add(item);
return list;
}
}Iterator Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Iterator Pattern in the Real World
“Think of a TV remote control. Whether your playlist is on a Blu-ray disc, a streaming service, or a USB drive, you use the same next and previous buttons to cycle through content. The remote (iterator interface) abstracts away the completely different internal mechanisms each media source uses to retrieve the next item.”