C#
IEnumerable
Generics
Programming
.NET

How do I implement IEnumerableT

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

Introduction

Implementing IEnumerable<T> in C# is what allows your custom collection to work with foreach, LINQ, and the broader .NET iteration ecosystem. The key requirement is simple: your type must provide a generic enumerator, and it should also satisfy the non-generic IEnumerable contract for compatibility.

The Minimal Contract

To implement IEnumerable<T>, your class must provide:

  • 'IEnumerator<T> GetEnumerator()'
  • 'IEnumerator IEnumerable.GetEnumerator()'

The non-generic method is usually implemented explicitly and forwards to the generic one.

A practical custom collection looks like this:

csharp
1using System.Collections;
2using System.Collections.Generic;
3
4public class NumberBag : IEnumerable<int>
5{
6    private readonly List<int> _items = new();
7
8    public void Add(int value) => _items.Add(value);
9
10    public IEnumerator<int> GetEnumerator()
11    {
12        foreach (var item in _items)
13        {
14            yield return item;
15        }
16    }
17
18    IEnumerator IEnumerable.GetEnumerator()
19    {
20        return GetEnumerator();
21    }
22}

That is enough for this code to work:

csharp
1var bag = new NumberBag();
2bag.Add(10);
3bag.Add(20);
4
5foreach (var item in bag)
6{
7    Console.WriteLine(item);
8}

Why yield return Is So Useful

yield return is the easiest way to implement enumeration because the compiler generates the state machine for you. You do not have to manually build an enumerator class unless you need a very specialized iteration pattern.

That makes most custom collection implementations short and readable.

If your type already wraps another collection internally, forwarding with yield return or returning the inner enumerator is usually the cleanest design.

A More Direct Enumerator Forwarding Pattern

If you do not need custom iteration logic, you can return the underlying collection’s enumerator directly:

csharp
1using System.Collections;
2using System.Collections.Generic;
3
4public class NameList : IEnumerable<string>
5{
6    private readonly List<string> _names = new();
7
8    public void Add(string name) => _names.Add(name);
9
10    public IEnumerator<string> GetEnumerator() => _names.GetEnumerator();
11
12    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
13}

This is more concise than yield return when the wrapped collection already behaves exactly the way you want.

When You Need a Custom Enumerator

Sometimes iteration is not just "walk through a list." You may need to:

  • skip null or invalid items
  • expose a computed sequence
  • traverse a tree or graph
  • iterate lazily over generated values

In those cases, yield return still works very well.

Example with filtering:

csharp
1public IEnumerator<int> GetEnumerator()
2{
3    foreach (var item in _items)
4    {
5        if (item >= 0)
6            yield return item;
7    }
8}

Now the collection exposes only non-negative values through enumeration, even if it stores more internally.

Relationship to ICollection<T> and IReadOnlyCollection<T>

IEnumerable<T> only promises iteration. It does not promise count, indexing, add, remove, or mutation behavior.

If your type is a full collection, you may also want to implement:

  • 'ICollection<T>'
  • 'IReadOnlyCollection<T>'
  • 'IList<T> or IReadOnlyList<T>'

But you do not need those interfaces just to support foreach.

Common Pitfalls

The biggest mistake is implementing only the generic GetEnumerator() and forgetting the non-generic IEnumerable.GetEnumerator(). Some older APIs and non-generic consumers still rely on that interface.

Another issue is exposing an enumerator over a collection that can be modified unsafely during iteration. If the underlying storage changes, enumeration behavior may become invalid or throw.

Developers also sometimes implement IEnumerable<T> on a type that is really a one-shot stream rather than a reusable collection. That can confuse callers who expect enumeration to be repeatable.

Finally, do not write a custom enumerator class unless you truly need it. In most cases, yield return is simpler and less error-prone.

Summary

  • Implement both the generic and non-generic enumeration methods.
  • Use yield return for the simplest and most readable custom iteration logic.
  • Forward to an inner collection enumerator when that already matches the desired behavior.
  • 'IEnumerable<T> means "can be iterated," not necessarily "full mutable collection."'
  • Keep enumeration behavior predictable, especially when underlying data can change.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

All Rights Reserved.