BrandGhost
How to Implement Iterator Pattern in C#: Step-by-Step Guide

How to Implement Iterator Pattern in C#: Step-by-Step Guide

How to Implement Iterator Pattern in C#: Step-by-Step Guide

Knowing the theory behind a design pattern is one thing -- but the real payoff comes when you can wire it up in your own codebase. If you want to implement iterator pattern in C#, you need to understand the interfaces, the mechanics of enumeration, and the shortcuts that the language gives you. This guide walks through every step, from the raw interface contracts all the way to custom LINQ-compatible collections.

The iterator pattern decouples the traversal logic from the collection itself, letting you iterate over a data structure without exposing its internal representation. C# bakes this pattern deep into the language through IEnumerable<T> and IEnumerator<T>, which means once you implement it correctly, you get foreach loops, LINQ queries, and the entire .NET ecosystem for free. If you want to see how other patterns layer on similar principles of separating responsibilities, the strategy pattern takes a comparable approach to decoupling algorithms from the code that uses them.

In this step-by-step guide, we'll build a custom priority collection, implement both manual and simplified iterators, add multiple traversal modes, and wire it all up with LINQ. By the end, you'll have a thorough understanding of how to implement iterator pattern in C# for real-world scenarios.

Step 1: Understand IEnumerable and IEnumerator

Before writing any code, you need to understand the two interfaces that form the foundation of the iterator pattern in C#. These are IEnumerable<T> and IEnumerator<T>, and they map directly to the Gang of Four's aggregate and iterator roles.

IEnumerable<T> is the aggregate. It represents a collection that can be iterated over. Its single method, GetEnumerator(), acts as a factory -- it creates and returns an iterator object each time it's called. This is what makes foreach work. When you write a foreach loop, the compiler calls GetEnumerator() behind the scenes and uses the returned enumerator to walk through the elements.

IEnumerator<T> is the iterator itself. It maintains the current position within the collection and provides three key members:

  • Current: Returns the element at the current position.
  • MoveNext(): Advances the position by one element and returns true if there's an element to read, or false when the sequence is exhausted.
  • Reset(): Resets the enumerator to its initial position (before the first element). In practice, many implementations throw NotSupportedException here.

There's also IDisposable to consider. IEnumerator<T> extends IDisposable, which means the foreach loop automatically disposes the enumerator when iteration completes or is interrupted. This is critical for iterators that hold resources like database connections or file handles.

The contract is simple: call GetEnumerator() on the collection, then loop by calling MoveNext() and reading Current until MoveNext() returns false. This separation between the "what" (the collection) and the "how" (the traversal logic) is the core idea behind the iterator pattern.

Step 2: Build a Custom Collection

Let's implement the iterator pattern in C# by building a realistic custom collection. We'll create a PriorityBucket<T> -- a collection that stores items grouped by priority level and iterates them from highest priority to lowest. This is more interesting than a simple list wrapper because the internal structure (a dictionary of lists) differs from the external iteration order.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

public sealed class PriorityBucket<T> : IEnumerable<T>
{
    private readonly SortedDictionary<int, List<T>> _buckets;

    public PriorityBucket()
    {
        _buckets = new SortedDictionary<int, List<T>>(
            Comparer<int>.Create((a, b) => b.CompareTo(a)));
    }

    public int Count => _buckets
        .Values
        .Sum(list => list.Count);

    public void Add(T item, int priority)
    {
        if (!_buckets.TryGetValue(priority, out var list))
        {
            list = new List<T>();
            _buckets[priority] = list;
        }

        list.Add(item);
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new PriorityBucketEnumerator<T>(_buckets);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

A few things to notice here. The SortedDictionary uses a custom comparer that sorts keys in descending order, so highest-priority items come first. The class implements both IEnumerable<T> and the non-generic IEnumerable -- you always need both, and the non-generic version just delegates to the generic one.

The GetEnumerator() method creates a new PriorityBucketEnumerator<T> instance each time it's called. This factory-per-call approach means multiple foreach loops over the same collection won't interfere with each other. If you've worked with dependency injection through IServiceCollection, this pattern of creating fresh instances on demand will feel familiar.

Step 3: Implement IEnumerator Manually

This is the step where many developers stumble when they implement the iterator pattern in C#. Writing a manual enumerator means tracking state yourself -- which bucket you're in, which index within that bucket, and handling the edge cases around initialization and disposal.

using System;
using System.Collections;
using System.Collections.Generic;

public sealed class PriorityBucketEnumerator<T>
    : IEnumerator<T>
{
    private readonly IList<KeyValuePair<int, List<T>>> _snapshot;
    private int _bucketIndex;
    private int _itemIndex;
    private bool _disposed;

    public PriorityBucketEnumerator(
        SortedDictionary<int, List<T>> buckets)
    {
        _snapshot = new List<KeyValuePair<int, List<T>>>(
            buckets);
        _bucketIndex = 0;
        _itemIndex = -1;
    }

    public T Current
    {
        get
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(
                    nameof(PriorityBucketEnumerator<T>));
            }

            if (_bucketIndex >= _snapshot.Count
                || _itemIndex < 0)
            {
                throw new InvalidOperationException(
                    "Enumerator is not positioned on " +
                    "a valid element.");
            }

            return _snapshot[_bucketIndex]
                .Value[_itemIndex];
        }
    }

    object? IEnumerator.Current => Current;

    public bool MoveNext()
    {
        if (_disposed)
        {
            throw new ObjectDisposedException(
                nameof(PriorityBucketEnumerator<T>));
        }

        while (_bucketIndex < _snapshot.Count)
        {
            _itemIndex++;
            List<T> currentList =
                _snapshot[_bucketIndex].Value;

            if (_itemIndex < currentList.Count)
            {
                return true;
            }

            _bucketIndex++;
            _itemIndex = -1;
        }

        return false;
    }

    public void Reset()
    {
        _bucketIndex = 0;
        _itemIndex = -1;
    }

    public void Dispose()
    {
        _disposed = true;
    }
}

Let's break down the important details:

  • Snapshot on construction: The enumerator copies the dictionary entries into a list at construction time. This protects against the collection being modified during iteration, which would otherwise throw InvalidOperationException.
  • Two-level indexing: Because the internal structure has buckets containing items, MoveNext() needs to advance within a bucket and then jump to the next bucket when one is exhausted. The while loop handles this cleanly.
  • Current guards: Accessing Current before calling MoveNext() or after the enumerator is disposed throws an appropriate exception. These guards prevent subtle bugs.

The IDisposable implementation here is minimal because we're not holding unmanaged resources. In a more complex scenario -- say, iterating over database rows or file lines -- Dispose() would close the underlying connection or handle. This is why IEnumerator<T> requires IDisposable in the first place.

This works perfectly, but look at how much code is needed just to walk through a collection. That's where yield return comes in.

Step 4: Simplify with yield return

If you want to implement the iterator pattern in C# with minimal ceremony, yield return is your best friend. It lets you write what looks like a simple loop, and the compiler generates the entire state machine -- the IEnumerator<T> implementation, the MoveNext() logic, the Current property, even Dispose() -- behind the scenes.

Here's the refactored GetEnumerator() method for PriorityBucket<T>:

public IEnumerator<T> GetEnumerator()
{
    foreach (var bucket in _buckets)
    {
        foreach (T item in bucket.Value)
        {
            yield return item;
        }
    }
}

That's it. Five lines replace the entire PriorityBucketEnumerator<T> class we built in Step 3. The yield return statement tells the compiler to pause execution, return the current item to the caller, and resume from the same point when MoveNext() is called again.

What the compiler generates under the hood is roughly equivalent to the manual enumerator we wrote -- a class that implements IEnumerator<T> with state-tracking fields and a MoveNext() method structured as a state machine. The difference is that you don't have to maintain that code. The compiler handles the edge cases, the disposal, and the state transitions.

A few things to keep in mind about yield return:

  • Deferred execution: The body of the method doesn't run until something starts iterating. If you need to validate arguments eagerly, split the method into a public wrapper that validates and a private iterator method that yields.
  • No random access: yield return produces a forward-only sequence.
  • Automatic disposal: If the iterator method contains a try/finally block, the finally runs when the enumerator is disposed.

The dramatic reduction in code is why most C# developers prefer yield return over manual enumerator classes. You should still understand the manual approach -- it's essential for debugging and for edge cases -- but for the majority of scenarios, let the compiler do the heavy lifting.

Step 5: Support Multiple Iteration Modes

One of the most powerful aspects of the iterator pattern is that a single collection can expose multiple ways to traverse its elements. When you implement the iterator pattern in C# with yield return, adding new traversal modes is as simple as adding new methods that return IEnumerable<T>.

Let's add reverse, filtered, and flattened-with-priority iteration to PriorityBucket<T>:

public IEnumerable<T> Reversed()
{
    foreach (var bucket in _buckets.Reverse())
    {
        for (int i = bucket.Value.Count - 1; i >= 0; i--)
        {
            yield return bucket.Value[i];
        }
    }
}

public IEnumerable<T> WithMinimumPriority(
    int minimumPriority)
{
    foreach (var bucket in _buckets)
    {
        if (bucket.Key < minimumPriority)
        {
            yield break;
        }

        foreach (T item in bucket.Value)
        {
            yield return item;
        }
    }
}

public IEnumerable<(T Item, int Priority)>
    WithPriorities()
{
    foreach (var bucket in _buckets)
    {
        foreach (T item in bucket.Value)
        {
            yield return (item, bucket.Key);
        }
    }
}

Each method returns IEnumerable<T> (or a related type) and uses yield return to produce elements lazily. Here's what each does:

  • Reversed() iterates from lowest priority to highest, and within each bucket, from last to first. This reverses the entire traversal order without duplicating or sorting the underlying data.
  • WithMinimumPriority() filters out items below a threshold. Notice the use of yield break -- it terminates iteration early once the priority drops below the minimum. Because the dictionary is sorted in descending order, once we hit a key below the threshold, there's no point continuing.
  • WithPriorities() returns tuples that pair each item with its priority level. This demonstrates that your iterators aren't limited to the collection's element type -- you can project into any shape you need.

Here's how consuming code uses these different modes:

var tasks = new PriorityBucket<string>();
tasks.Add("Fix critical bug", priority: 10);
tasks.Add("Write unit tests", priority: 5);
tasks.Add("Update README", priority: 1);
tasks.Add("Deploy hotfix", priority: 10);

Console.WriteLine("--- Default (High to Low) ---");
foreach (string task in tasks)
{
    Console.WriteLine(task);
}

Console.WriteLine("--- Reversed ---");
foreach (string task in tasks.Reversed())
{
    Console.WriteLine(task);
}

Console.WriteLine("--- Priority 5+ ---");
foreach (string task in tasks.WithMinimumPriority(5))
{
    Console.WriteLine(task);
}

Console.WriteLine("--- With Priorities ---");
foreach (var entry in tasks.WithPriorities())
{
    Console.WriteLine(
        $"[P{entry.Priority}] {entry.Item}");
}

The collection itself never changes. The iteration logic is externalized into separate methods, each producing a different view of the same data. This is the iterator pattern at its most flexible. If you've used the composite pattern for tree structures, you'll recognize a similar idea -- the composite can be traversed depth-first or breadth-first through different iterators over the same structure.

Step 6: Make It LINQ-Compatible

Once you implement the iterator pattern in C# by implementing IEnumerable<T>, LINQ compatibility comes for free. Every LINQ operator -- Where, Select, OrderBy, Aggregate, Any, Count -- works immediately against your custom collection because LINQ is built on top of IEnumerable<T>.

var tasks = new PriorityBucket<string>();
tasks.Add("Fix critical bug", priority: 10);
tasks.Add("Write unit tests", priority: 5);
tasks.Add("Update README", priority: 1);
tasks.Add("Deploy hotfix", priority: 10);
tasks.Add("Refactor service layer", priority: 7);

// Standard LINQ queries work out of the box
var highPriorityTasks = tasks
    .WithPriorities()
    .Where(entry => entry.Priority >= 7)
    .Select(entry => entry.Item)
    .ToList();

int totalTasks = tasks.Count();

bool hasLowPriority = tasks
    .WithPriorities()
    .Any(entry => entry.Priority < 3);

Console.WriteLine(
    $"High priority: {string.Join(", ", highPriorityTasks)}");
Console.WriteLine($"Total: {totalTasks}");
Console.WriteLine($"Has low priority: {hasLowPriority}");

You don't need to add anything special to your collection for this to work. LINQ extension methods target IEnumerable<T>, and since PriorityBucket<T> implements that interface, the entire LINQ toolbox is available.

Writing a Custom Extension Method

You can extend this further by writing your own LINQ-style extension methods using yield return. Here's a BatchBy method that groups a sequence into fixed-size batches:

public static class EnumerableExtensions
{
    public static IEnumerable<IReadOnlyList<T>> BatchBy<T>(
        this IEnumerable<T> source,
        int batchSize)
    {
        if (batchSize <= 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(batchSize),
                "Batch size must be positive.");
        }

        var batch = new List<T>(batchSize);

        foreach (T item in source)
        {
            batch.Add(item);

            if (batch.Count == batchSize)
            {
                yield return batch;
                batch = new List<T>(batchSize);
            }
        }

        if (batch.Count > 0)
        {
            yield return batch;
        }
    }
}

And using it against our priority collection:

foreach (var batch in tasks.BatchBy(2))
{
    Console.WriteLine(
        $"Batch: {string.Join(", ", batch)}");
}

This custom extension method follows the same yield return mechanics we used inside the collection itself. It's composable -- you can chain BatchBy with Where, Select, and any other LINQ operator because they all speak the same IEnumerable<T> language. If you're building complex processing pipelines, the template method pattern offers a complementary approach for defining algorithm skeletons while letting subclasses fill in the steps.

Common Implementation Mistakes

Even with yield return simplifying most of the work, there are several pitfalls that catch developers when they implement the iterator pattern in C#.

Modifying the collection during iteration: This is the most common mistake. If you add or remove items from a collection while a foreach loop is active, .NET throws InvalidOperationException. The manual enumerator in Step 3 handled this by taking a snapshot. When using yield return, any structural change after iteration begins causes an exception. If you need to modify a collection during traversal, iterate over a copy or collect the modifications and apply them after the loop.

Not implementing IDisposable properly: IEnumerator<T> inherits from IDisposable. If your enumerator holds resources like database readers or file streams, failing to implement Dispose() correctly leads to resource leaks. The foreach statement calls Dispose() in a finally block automatically, but only if your enumerator supports it.

Forgetting thread safety: Iterators are not thread-safe by default. If one thread is iterating while another modifies the collection, you get either an exception or corrupted state. Solutions include using concurrent collections from System.Collections.Concurrent, taking a snapshot before iteration, or using explicit locking.

Creating infinite iterators without termination: yield return makes it trivial to create infinite sequences. That's powerful for things like Fibonacci generators, but if consuming code uses ToList() or Count() on an infinite sequence without a Take() first, the application hangs. Always document whether a method can return an infinite sequence.

Skipping eager argument validation: Because yield return methods are lazily evaluated, argument validation inside the method body doesn't execute until iteration begins. This means passing null to an iterator method won't throw until someone calls MoveNext(). The fix is to split the method into a public method that validates eagerly and a private method that does the yielding.

Frequently Asked Questions

What is the difference between IEnumerable and IEnumerator in C#?

IEnumerable<T> represents a collection that can be iterated. Its sole purpose is to produce an IEnumerator<T> through its GetEnumerator() method. Think of IEnumerable<T> as the factory and IEnumerator<T> as the product -- the factory creates a fresh cursor each time, and that cursor tracks position through the sequence.

Should I implement IEnumerator manually or use yield return?

Use yield return for the vast majority of cases. It produces less code, fewer bugs, and handles disposal automatically. Manual IEnumerator<T> implementations are worth considering when you need precise control over state transitions or when you're building high-performance iterators that need to avoid allocations. If you implement iterator pattern in C# for a typical business application, yield return is the right default.

Can I iterate over a custom collection with foreach without implementing IEnumerable?

Technically, yes. The C# foreach statement uses duck typing -- it looks for a public GetEnumerator() method that returns an object with MoveNext() and Current, regardless of whether IEnumerable<T> is implemented. However, skipping IEnumerable<T> means your collection won't work with LINQ and won't be accepted by methods that take IEnumerable<T> parameters. In practice, always implement the interface.

How does yield return work under the hood?

The compiler transforms a yield return method into a class that implements IEnumerator<T>. This generated class contains a state machine with fields tracking where execution paused. Each call to MoveNext() jumps to the correct state, executes code until the next yield return or yield break, and saves the new state. Local variables become fields on the generated class so their values persist across calls. You can inspect the generated code using SharpLab or ILSpy.

How do I make my custom iterator thread-safe?

The safest approach is to take an immutable snapshot of the collection data when GetEnumerator() is called. This is what the manual enumerator in Step 3 does by copying the dictionary entries into a list. With this approach, modifications to the original collection after iteration starts don't affect the iterator and don't throw exceptions. For higher concurrency requirements, consider using collections from System.Collections.Concurrent or wrapping access in a ReaderWriterLockSlim. The flyweight pattern offers a related perspective on managing shared state efficiently across many consumers.

What happens if I call yield return inside a try-catch block?

You can use yield return inside a try/finally block, and the finally will execute when the enumerator is disposed. However, you cannot use yield return inside a try/catch block -- this is a compiler restriction. If you need exception handling within an iterator method, refactor the risky operation into a separate non-iterator method and call it from within the iterator. The yield break statement, which terminates iteration early, works fine in any context.

When should I prefer the iterator pattern over returning a List or Array?

Use the iterator pattern when you want lazy evaluation -- producing elements on demand rather than computing the entire result upfront. This matters when the sequence is large, expensive to compute, or potentially infinite. Returning a List<T> or array is appropriate when the consumer needs random access, will iterate multiple times, or when the data set is small enough that materializing it costs nothing. The decorator pattern offers a similar trade-off -- composing behavior lazily at runtime versus locking it in at compile time.

Iterator Design Pattern in C#: Complete Guide with Examples

Master the iterator design pattern in C# with practical examples showing IEnumerable, IEnumerator, custom iterators, yield return, and real-world .NET implementations.

How to Implement Singleton Pattern in C#: Step-by-Step Guide

How to implement Singleton pattern in C#: step-by-step guide with code examples, thread-safe implementation, and best practices for creational design patterns.

How to Implement Strategy Pattern in C#: Step-by-Step Guide

How to implement Strategy pattern in C#: step-by-step guide with code examples, best practices, and common pitfalls for behavioral design patterns.

An error has occurred. This application may no longer respond until reloaded. Reload