Updated on

IEnumerable acts as an abstraction over a collection and allows us to iterate over it without knowing the actual type of the collection. It has one method, GetEnumerator(), and that method is what foreach and LINQ build on.

To download the source code for this article, you can visit our GitHub repository.

Let’s begin.

What Is the IEnumerable Interface in C#?

IEnumerable is the interface in System.Collections that marks a type as something we can enumerate. It is the base for all the non-generic collections that can be enumerated, and its generic sibling IEnumerable<T> extends it, so every array, List<T> and Dictionary<TKey, TValue> is an IEnumerable too.

It exposes a single method, GetEnumerator(). This method returns a reference to yet another interface, System.Collections.IEnumerator.

The IEnumerator interface does the actual walking. MoveNext() advances to the next element and returns false once there are no more. The Current property returns the element at the current position. Reset() moves back to the start.

The split is deliberate. The collection holds the data, and each enumerator holds one position in it, so two loops over the same collection can keep their own place.

foreach hides all of this from us. The compiler calls GetEnumerator() once, then calls MoveNext() and reads Current until the loop runs out of elements.

If you’d like to learn about the different collection types available in .NET, check out our guide to C# collections.

Implementing IEnumerable is the standard way to make our own collection work with foreach. Strictly speaking, the compiler only needs a public GetEnumerator() method whose return type has Current and MoveNext(). Microsoft’s C# reference says so: “The foreach statement isn’t limited to those types.” Our Library class below relies on that rule.

Which LINQ Methods Work on a Non-Generic IEnumerable?

LINQ adds its operators as extension methods, and almost all of them extend the generic IEnumerable<T> or a type built on it. Only four accept the non-generic IEnumerable: Cast<TResult>(), OfType<TResult>(), AsParallel() and AsQueryable(). We checked by reflection over Enumerable, ParallelEnumerable and Queryable on .NET 10.0.10 (2026-10-17).

Cast<TResult>() and OfType<TResult>() are the bridge from an old non-generic collection into the rest of LINQ. AsQueryable() is stricter than its signature: it throws an ArgumentException unless the object also implements IEnumerable<T>. None of the four runs the query when we call it. The work happens only when we enumerate the result, either with a foreach loop or by calling its GetEnumerator() method.

Cast<TResult>

This method takes a sequence of type IEnumerable and converts all the objects of the given sequence to the type TResult, thus resulting in a return type IEnumerable<TResult>.

The Cast<TResult>() method is a generic method where TResult acts as a placeholder for the actual type we want to work with at compile time. It allows the use of standard LINQ query operators like filtering, sorting, etc. on a non-generic type such as ArrayList. We write each LINQ example as the body of an xUnit test, in a test file that starts with using System.Collections;:

var primes = new ArrayList
{
    5,
    3,
    2,
    7
};

var primeQuery = primes.Cast<int>().OrderBy(prime => prime).Select(prime => prime);

var expected = new List<int> { 2, 3, 5, 7 };

Assert.Equal(expected, primeQuery);

We are able to convert a non-generic collection of type ArrayList into IEnumerable<int> and thus have all its functionalities available.

OfType<TResult>

This method takes a sequence of type IEnumerable and filters it based on the specified type. It returns only those elements from the input source that can be cast to the type TResult:

var cities = new ArrayList
{
    "London",
    "Paris",
    "Madrid",
    "Berlin",
    7,
    "Lisbon"
};

var cityQuery = cities.OfType<string>();

var expected = new List<string> { "London", "Paris", "Madrid", "Berlin", "Lisbon" };

Assert.Equal(expected, cityQuery);

This is where it differs from Cast<TResult>(), which throws an InvalidCastException for an element it can’t cast, at the moment we enumerate the result. We can also use standard query operators such as Where() after filtering the source:

var cities = new ArrayList
{
    "London",
    "Paris",
    "Madrid",
    "Berlin",
    7,
    "Lisbon",
    8,
    12
};

var evens = cities.OfType<int>().Where(x => x % 2 == 0);

var expected = new List<int> { 8, 12 };

Assert.Equal(expected, evens);

AsParallel Method

This method enables the parallelization of a query. It takes a sequence of type IEnumerable and converts it to the type ParallelQuery, or to ParallelQuery<T> when the sequence is generic, as ours is:

var numbers = Enumerable.Range(0, 100);

var oddsCount = numbers.AsParallel().Count(x => x % 2 != 0);

var expected = 50;

Assert.Equal(expected, oddsCount);

The AsParallel() method binds the query to PLINQ or Parallel LINQ. The PLINQ queries work in a similar way to the sequential LINQ queries where they operate on in-memory data sources and allow deferred execution.

However, they attempt to make use of all available processors by partitioning the data source into segments and then executing the query on each segment on separate worker threads in parallel on multiple processors. 

We go deeper into partitioning and when parallel queries pay off in our article on PLINQ in .NET.

AsQueryable Method

This method takes a sequence of type IEnumerable and converts it into IQueryable, or into IQueryable<T> when the sequence is generic, as our list is:

var numbers = new List<int>
{
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10
};

var query = numbers.AsQueryable();

var multiplesOf3 = query.Where(x => x % 3 == 0);

var expected = new List<int> { 3, 6, 9 };

Assert.Equal(expected, multiplesOf3);

Assert.Equal("Constant", query.Expression.NodeType.ToString());

The IQueryable interface extends the IEnumerable interface and hence is similar in functionality.

The main difference between the two is where the query runs. IEnumerable runs LINQ operators in memory, inside our process. IQueryable carries the query as an expression tree and leaves the execution to its provider, so EF Core can translate it into SQL and send back only the filtered rows.

Over an in-memory list, as in our example, AsQueryable() wraps the list in an EnumerableQuery<T> that still runs in memory. That is why the expression’s NodeType is Constant: the whole query starts from a constant that holds the EnumerableQuery<T> itself.

That covers the four LINQ methods for a non-generic IEnumerable. Now let’s implement the interface ourselves.

How Do We Implement IEnumerable in C#?

Implementing IEnumerable takes two pieces: the collection class and an enumerator that walks it.

The collection class implements IEnumerable and its one method, GetEnumerator(). That method returns a new enumerator every time we call it, so each loop starts fresh.

The enumerator class implements IEnumerator. It keeps an index that starts before the first element, at -1. MoveNext() increments the index and returns whether it still points inside the collection. Current returns the element at the index. Reset() puts the index back to -1.

That is the whole contract. Writing it by hand once is the clearest way to see what foreach does for us on every loop.

In everyday code, we rarely write the enumerator ourselves. We implement the generic IEnumerable<T> instead and let a yield return loop generate the enumerator. That version also works with LINQ directly, with no Cast<TResult>() call in front of every query.

Let’s start by creating a Book record:

public record Book(string Name, string Author);

Now, that we have our business object, let’s create a collection of this object:

using System.Collections;

public class Library : IEnumerable
{
    private readonly Book[] _books;

    public Library(Book[] books)
    {
        _books = [.. books];
    }
}

The class Library implements IEnumerable so that we can use a foreach loop on it. The collection expression [.. books] copies the array, so later changes to the caller’s array don’t leak into our library. However, the code does not compile until we implement the GetEnumerator() method of the interface.

As we need an IEnumerator when implementing IEnumerable, let’s first implement the IEnumerator interface:

using System.Collections;

public class BookEnumerator : IEnumerator
{
    private readonly Book[] _books;
    private int _index = -1;

    public BookEnumerator(Book[] books)
    {
        _books = books;
    }

    public Book Current
    {
        get
        {
            return _books[_index];
        }
    }

    object IEnumerator.Current => Current;

    public bool MoveNext()
    {
        _index++;
        return _index < _books.Length;
    }

    public void Reset()
    {
        _index = -1;
    }
}

We start by implementing the required members of IEnumerator in the class BookEnumerator i.e. the property Current, and the methods Reset() and MoveNext().

When the class is instantiated, the enumerator _index is placed before the first element of the collection. That is the job of the -1 initializer. The first call to MoveNext() moves _index to 0, the first book. Without it, _index starts at 0 and the first MoveNext() jumps straight to the second book. Microsoft’s IEnumerator documentation states the rule every enumerator follows: “Initially, the enumerator is positioned before the first element in the collection.” When iterating over the collection, we send a call to MoveNext() that advances the enumerator to the first position. 

The Current property returns the element at the current position of the enumerator. It returns the same value until MoveNext() or Reset() are called. It is undefined under these conditions:

  • When the enumerator is positioned before the first element of the collection, immediately after the instantiation
  • When the last call to MoveNext() returns false i.e. the iteration of the collection is complete
  • If any change in the collection such as addition, modification, etc. invalidates the enumerator

The Reset() method resets the enumerator position to the original. Microsoft documents Reset() as being there for COM interoperability, and an implementation may throw a NotSupportedException instead. The enumerators the compiler generates for yield return do exactly that. The MoveNext() method advances _index on every call, and once it reaches the position after the last element of the collection, the MoveNext() method always returns false.

With this step done, we are now ready to revisit our Library class and get rid of the compiler error due to not implementing the GetEnumerator() method:

public class Library : IEnumerable
{
    //Existing Code excluded for brevity

    public BookEnumerator GetEnumerator() => new BookEnumerator(_books);
        
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

}

Notice that our public GetEnumerator() returns BookEnumerator, not IEnumerator. When we write a foreach over a Library, the compiler picks this public method first, so the loop variable is a Book and book.Name compiles without a cast. The explicit IEnumerable.GetEnumerator() is there for code that only knows the interface. Through the interface, the loop variable is just object.

Now let’s see the Library class in action as an implementation of IEnumerable:

Book[] books =
[
    new("Anna Karenina", "Leo Tolstoy"),
    new("To Kill a Mockingbird", "Harper Lee"),
    new("The Great Gatsby", "F. Scott Fitzgerald"),
    new("One Hundred Years of Solitude", "Gabriel García Márquez"),
    new("A Passage to India", "E.M. Forster")
];

var library = new Library(books);

foreach (var book in library)
{
    Console.WriteLine($"Book: {book.Name}, Author: {book.Author}");
}

Thanks to GetEnumerator(), we can use a foreach loop on our Library. On running the program we receive the expected output:

Book: Anna Karenina, Author: Leo Tolstoy
Book: To Kill a Mockingbird, Author: Harper Lee
Book: The Great Gatsby, Author: F. Scott Fitzgerald
Book: One Hundred Years of Solitude, Author: Gabriel García Márquez
Book: A Passage to India, Author: E.M. Forster

How Do We Implement IEnumerable<T> With yield return?

The hand-written enumerator shows every moving part, but it is not what we usually write today. Since C# 2.0, the compiler can write the enumerator for us.

Let’s create a GenericLibrary class that implements the generic IEnumerable<Book> interface:

using System.Collections;

public class GenericLibrary : IEnumerable<Book>
{
    private readonly Book[] _books;

    public GenericLibrary(Book[] books)
    {
        _books = [.. books];
    }

    public IEnumerator<Book> GetEnumerator()
    {
        foreach (var book in _books)
        {
            yield return book;
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

Here, the yield return statement hands back one book at a time. The compiler turns our GetEnumerator() into a class that implements IEnumerator<Book>, with Current, MoveNext() and Dispose() written for us. We still add the explicit non-generic GetEnumerator(), because IEnumerable<T> extends IEnumerable.

LINQ is where the generic version helps most. Our non-generic Library needs a Cast<Book>() call before any query, which is the job of Cast<TResult>() from earlier. GenericLibrary works with LINQ directly:

var genericLibrary = new GenericLibrary(books);

var tolstoy = genericLibrary.Where(book => book.Author == "Leo Tolstoy");

foreach (var book in tolstoy)
{
    Console.WriteLine($"By Tolstoy: {book.Name}");
}

Running the program now prints one more line after the library list:

By Tolstoy: Anna Karenina

The generated enumerator does not reset: calling Reset() on it throws a NotSupportedException. To iterate again, we ask for a new enumerator, which is what every new foreach does anyway.

To stop an iterator early, we use the yield break statement. And when the items arrive asynchronously, for example from a database or a web API, the next step is IAsyncEnumerable with yield.

Conclusion

In this article, we learned about IEnumerable in C#. We saw how GetEnumerator() and IEnumerator work together to let foreach walk a collection, which four LINQ methods accept a non-generic IEnumerable, and how to implement the interface twice: by hand with our own enumerator, and with yield return on the generic IEnumerable<T>.

If you’re choosing which collection interface to expose from your own API, our comparison of IEnumerable vs ICollection vs IList vs List is the natural next read.

Tested with .NET 10.0.10 (SDK 10.0.302) and xUnit 2.9.3.