Linq Behind the Scenes
By: David Klempfner
Tags: Software Engineering, Dotnet Core, Programming, Software Development, Dotnet

Linq Behind the Scenes

Photo by Matteo Catanese on Unsplash

How many source “iterations” would happen when the below code executes?

Hypothesis

Most people think one of two things:

The source is iterated just once, so three iterations/loops for a source with three items. Nine iterations (each extension method iterates the collection).

Let’s find out the correct answer.

What is an iteration?

We first need to agree on the definition of an iteration.

Most people think of an iteration as one loop through a foreach/for/while etc.

But what happens when you loop through a foreach?

Behind the foreach syntactic sugar

Behind the scenes, the iterator pattern is implemented.

The above foreach loop is syntactic sugar for:

A foreach loop can be thought of as a call to GetEnumerator() , a while loop, a call to MoveNext() and accessing the Current property.

Iteration definition

The actual iteration can be thought of as a call to MoveNext() that returns true (because if it returns false , the foreach won’t iterate), and reading the Current property.

This is important to remember for the rest of the article.

Linq methods behind the scenes

Let’s get back to the original code snippet:

To understand what’s happening here, let’s strip away the syntactic sugar.

No syntactic sugar

The above code snippet will compile to the same CIL code, had you written it this way:

The above was generated using sharplab.io, with the ugly compiler generated names renamed to more user friendly names.

Here we can see under the covers of four pieces of syntactic sugar:

Lambda expressions become Func static class members. List initializers become individual class instantiations, each added using List.Add() . Extension methods become static method calls, in reverse written order. Auto properties become properties with backing fields.

The reason for stripping away the syntactic sugar, is because it makes it easier to view what’s going on with the Linq methods:

ToList(), Where() and Select()

Here we can see that the result of Select() is used as an input for Where() , and the result of Where() is used as an input for ToList() .

Select() takes the list, as well as an instance of the selector delegate, and Where() takes the result of that, as well as an instance of the predicate delegate.

Let’s examine the code for each of these.

In .NET 5, the Linq extension methods in Enumerable are spread over multiple files, which is why the partial keyword is used.

Select() just returns an instance of SelectListIterator<Foo, int> (an IEnumerable<T> ) which contains a reference to the source ( list ) and the selector func.

The Where() call ends up creating an instance of WhereEnumerableIterator<int> , passing in a reference this which is the SelectListIterator<Foo, int> acting as an Iterator .

One thing to note is that the SelectListIterator<Foo, int> has a TSource ( Foo ) and a TResult ( int ), however WhereEnumerableIterator<int> only has TSource specified, because the TResult is always bool .

Hierarchy

Before we look at ToList() , it might be helpful to visualise the object structure we have right now.

After Where() returns, and before ToList() is called, we have a WhereEnumerableIterator<int> instance at the highest level, containing a Func<int, bool> ( bar => bar < 4 ), and an IEnumerable<Foo> ( SelectListIterator<Foo, int> ).

Similarly, the SelectListIterator<Foo, int> contains a Func<Foo, int> and an IEnumerable<Foo> ( List<Foo> ).

Object composition/hierarchy.

Finally, ToList() is called on the WhereEnumerableIterator<int> , through the IEnumerable<Foo> interface, which calls WhereEnumerableIterator<int>.ToList() .

So far nothing really has happened, we’ve just had objects being created and returned.

Let’s see what happens in WhereEnumerableIterator<int>.ToList() .

As you can see, WhereEnumerableIterator<int>.ToList() iterates over the _source , which is a SelectListIterator<Foo, int> , adding each element to a local list .

The first iteration

You can follow these instructions if you want to debug the sourcecode yourself.

As we learnt earlier, the foreach in the above code snippet, is just syntactic sugar for a call to GetEnumerator() , a while loop calling MoveNext() on the enumerator, and accessing the Current property.

The first thing that happens as part of the foreach , is that GetEnumerator() is called on the SelectListIterator<Foo, int> ’s parent class, Iterator<Foo> . This simply returns itself, since it already is an IEnumerator<Foo> (as well as being an IEnumerable<Foo> ).

In a foreach , after GetEnumerator() is called, MoveNext() is called.

Don’t worry about the switch statement too much. Just know that _source.GetEnumerator() is called just once. Here _source is the original List<Foo> .

This is called by line 8 in the above snippet. This returns an instance of the Enumerator struct.

Back inside SelectListIterator<Foo, int>.MoveNext() , _enumerator.MoveNext() is called on line 12.

The main thing that happens here is that the _current property is assigned the next item in the _list .

Click here to find out why they introduced the local variable localList .

The process

We have all the code snippets we are interested in now.

Feel free to scroll up and view the relevant code snippet as we discuss what is happening.

After List<Foo>.Enumerator.MoveNext() is called, we start bubbling back up through the method calls.

The SelectListIterator<Foo, int> is kind of a middle man between the ToList() (where the iteration is initiated), and the underlying List<Foo> that we started off with.

Back in SelectListIterator<Foo, int>.MoveNext() , we assign its _current property with the value from the List<Foo> ’s Enumerator ’s _current property, based on the selector, which returns an int .

The SelectListIterator<Foo, int>.MoveNext() call will then return true , back to where it was called, inside the while loop (displaying as a foreach loop), inside ToList().

ToList() then uses its predicate, to determine whether or not the int is below 4. If it is, it’s added to the local list .

Simplified diagram of the “flow”.

How many iterations?

As you can see from the above diagram, there are only two MoveNext() methods that are called.

A foreach iteration only happens when MoveNext() returns true .

So let’s work out how many times these MoveNext() methods are called, and return true .

With three elements in the List<Foo> , we have 3 * 2 = 6 calls to MoveNext() that return true .

If you wrote the equivalent code with foreach and yield return , there would be six iterations (three in each foreach ).

What about in the .NET Framework?

.NET 5 is more optimised than the .NET Framework.

Majority of the code is quite similar to .NET 5. The main difference is the Where() and ToList() .

The WhereEnumerableIterator<int> doesn’t have an optimised ToList() method.

Instead it just implements the iterator pattern, using the predicate to return true or false within the MoveNext() method.

In .NET 5, we’d be done by now, but because in .NET Framework, WhereEnumerableIterator<int> doesn’t create the List<int> , ToList() has to do this for us, which is an extra step.

This just calls the List<int> constructor, passing in the IEnumerable<int> which is the WhereEnumerableIterator<int> .

Here is the extra MoveNext() call that .NET 5 doesn’t have.

Instead of combining the List<int> creation and the where predicate, they are done in separate steps.

The List<int> is now initialized, and we’re finished.

Summary

In .NET 5, there are only two equivalent foreach loops happening behind the scenes. However in .NET Framework, there’s an extra one happening, which means nine iterations happen, instead of six.

I hope you’ve enjoyed learning how Linq works behind the scenes.

Knowing the low level implementation details isn’t just interesting, it can help you write better code (I’m sure you know not to call ToList() unnecessarily).

If you have any questions, please feel free to leave a comment.