IQueryable<T> and IEnumerable<T>

IEnumerable<T>

Remember from the last course that IEnumerable<T> is an interface that represents a collection of objects that can be enumerated over. It's one of the main drivers of LINQ and is used to query, filter, and manipulate collections of data.

Example

public class Example
{
    public void PrintItems(IEnumerable<string> items)
    {
        //Remember foreach?
        foreach (var item in items)
        {
            Console.WriteLine(item);
        }
    }
}

A couple of important things to note (copied straight from the last course):

Lazy evaluation

Lazy evaluation is an important concept in .NET collections, especially when working with IEnumerable<T>. It means that values or sequences of values are not generated or fetched until they are actually needed, which can significantly improve performance and reduce memory usage, especially when working with large data sets or expensive computations.

How Lazy Evaluation Works with IEnumerable<T>

In the context of IEnumerable<T>, methods that operate on collections like Select, Where, and Take do not immediately execute when called. Instead, they set up a query pipeline, and the actual iteration (evaluation) happens only when you access the elements—for example, by using a foreach loop or calling a terminal operation like ToList().

This "deferred execution" model allows for efficient processing of potentially large collections without consuming memory unnecessarily by generating all elements up front.

public class Example
{
    public static void Main()
    {
        var numbers = new[] { 1, 2, 3, 4, 5 };
        
        var query = numbers.Where(n => n % 2 == 0);

        Console.WriteLine("Query is defined, but not executed yet.");

        foreach (var number in query)
        {
            Console.WriteLine(number);  // Only now is the query executed
        }
    }
}
  • In the example, numbers.Where(n => n % 2 == 0) defines the query but does not execute it immediately.
  • The query is only evaluated when the foreach loop iterates over the sequence. This is what makes the query "lazy."

IQueryable<T>

IQueryable<T> is an interface that represents a collection of objects that can be queried. It is a .NET standard interface that is implemented by many classes in the .NET framework, including DbSet<T> in Entity Framework Core.

IQueryable<T> is similar, but vastly different from IEnumerable<T> in one core way - it's meant to be used for querying data from a non-in-memory data source. That could be lots of things, but in .NET it's typically databases. It retains all of the good LINQ-ness that IEnumerable<T> has, but because it has a layer in between that can effectively translate your LINQ calls into the language of the target data source, it can do some pretty cool things.

But before we can talk about IQueryable<T>, we need to talk about expression trees briefly

What are the differences between these two lines of code?

Func<string, string> toLower = s => s.ToLower();
Expression<Func<string, string>> toLower = s => s.ToLower();

The difference is the first is a Func<string, string> which is a delegate that takes a string and returns a string.
The second is an Expression<Func<string, string>> which is a representation of a lambda expression that can be compiled into a delegate.

This is effectively a descriptor of a function.

  • Functions do the thing
  • Expression trees describe the thing that does the thing

Expression Trees

An Expression Tree is a representation of code in a tree structure. Each node in the tree represents an expression in the code.

🌶️🌶️🌶️ I have a whole talk I give on this subject. It's one of my favorite talks and it's worth a watch if you're curious about all of the things you can do with expression trees. See it on YouTube (it's 5 years old, but the content is still very relevant!): https://www.youtube.com/watch?v=Ptnxc6tVIPE

Ok, but why does this matter?

It has a big impact on how LINQ works when you query databases - we'll cover that much more in the coming lessons.