DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

IEnumerable vs IQueryable --- The Day This Finally Made Sense (2026 Edition)

IEnumerable vs IQueryable --- The Day This Finally Made Sense (2026 Edition)

IEnumerable vs IQueryable --- The Day This Finally Made Sense (2026 Edition)

Cristian Sifuentes\
Senior .NET Engineer · Performance & Data Access Specialist


TL;DR

IEnumerable executes in memory.\
IQueryable executes in the database.

That sentence is technically correct.

It is also dangerously incomplete.

This article is not about definitions.\
It is about execution pipelines, expression trees, query providers,
performance boundaries, and the architectural consequences of choosing
the wrong abstraction.

If you've ever written .ToList() too early, this is for you.


The Real Source of Confusion

Beginners struggle because the APIs look identical:

var result = source.Where(x => x.Age > 20);
Enter fullscreen mode Exit fullscreen mode

Same LINQ syntax.

Same extension methods.

Same return type shape.

Different runtime behavior.

What changes is not the method.

What changes is the provider behind the query.

Understanding that provider boundary is the moment everything clicks.


Part 1 --- IEnumerable: LINQ-to-Objects

Let's start with what feels normal.

List<User> users = GetUsers();

var adults = users.Where(x => x.Age > 20);
Enter fullscreen mode Exit fullscreen mode

At this point:

  • users is already materialized in memory
  • .Where() is an extension method from System.Linq
  • The filtering runs inside your .NET process
  • C# delegates execute against in-memory objects

There is no SQL.

There is no translation.

There is no expression tree provider.

This is LINQ-to-Objects.

What Actually Happens

The signature of Where for IEnumerable<T> is:

public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate);
Enter fullscreen mode Exit fullscreen mode

Notice the key detail:

Func<TSource, bool>
Enter fullscreen mode Exit fullscreen mode

That is a compiled delegate.

It runs directly in memory.

Your lambda becomes executable IL.

That is why this works:

var adults = users.Where(x => IsAdult(x));
Enter fullscreen mode Exit fullscreen mode

Where IsAdult is any arbitrary C# method.

Because everything happens locally.


Why IEnumerable Is Dangerous with Databases

Now imagine this:

var users = db.Users.ToList();

var adults = users.Where(x => x.Age > 20);
Enter fullscreen mode Exit fullscreen mode

What happened?

  1. db.Users.ToList() pulled every row from the database.
  2. Filtering happened in memory.
  3. You just loaded unnecessary data.

On small tables, you won't notice.

On large tables, you just created a performance incident.

The problem isn't IEnumerable.

The problem is premature materialization.


Part 2 --- IQueryable: LINQ-to-Provider

Now let's look at EF Core.

var adults = db.Users.Where(x => x.Age > 20);
Enter fullscreen mode Exit fullscreen mode

Notice what is different:

  • No ToList()
  • db.Users is IQueryable<User>
  • The lambda is not compiled yet

The signature of Where for IQueryable is:

public static IQueryable<TSource> Where<TSource>(
    this IQueryable<TSource> source,
    Expression<Func<TSource, bool>> predicate);
Enter fullscreen mode Exit fullscreen mode

The key difference:

Expression<Func<TSource, bool>>
Enter fullscreen mode Exit fullscreen mode

That is not a delegate.

It is an expression tree.


Expression Trees Change Everything

Instead of executing your lambda, EF Core analyzes it.

Example:

x => x.Age > 20
Enter fullscreen mode Exit fullscreen mode

Is converted into an expression tree like:

BinaryExpression
  Left: MemberExpression (x.Age)
  Right: ConstantExpression (20)
  Operator: GreaterThan
Enter fullscreen mode Exit fullscreen mode

EF Core then translates this tree into SQL:

SELECT * FROM Users WHERE Age > 20
Enter fullscreen mode Exit fullscreen mode

The filtering happens in the database engine.

Indexes apply.

Query planners optimize.

Network traffic reduces.

That is the real power of IQueryable.


Execution Timing: Deferred Execution

Both IEnumerable and IQueryable support deferred execution.

This means the query is not executed until you enumerate it.

Example:

var query = db.Users.Where(x => x.Age > 20);

// Nothing executed yet

var list = query.ToList(); // Now SQL runs
Enter fullscreen mode Exit fullscreen mode

Execution happens when:

  • ToList()
  • First()
  • Single()
  • Count()
  • foreach

Understanding this is critical for performance reasoning.


The Hidden Performance Killer: AsEnumerable()

Consider this:

var query = db.Users
    .Where(x => x.Age > 20)
    .AsEnumerable()
    .Where(x => ComplexMethod(x));
Enter fullscreen mode Exit fullscreen mode

What just happened?

Before AsEnumerable():

  • Everything is translated to SQL.

After AsEnumerable():

  • The provider switches to LINQ-to-Objects.
  • Remaining filters execute in memory.

This is sometimes intentional.

Often it is accidental.

That boundary line is architectural.


The Silent Bug: Calling .ToList() Too Early

Example:

var users = db.Users.ToList();

var filtered = users
    .Where(x => x.Age > 20)
    .Take(10);
Enter fullscreen mode Exit fullscreen mode

This translates to:

  • Load entire table
  • Filter in memory
  • Take 10 locally

Correct version:

var filtered = db.Users
    .Where(x => x.Age > 20)
    .Take(10)
    .ToList();
Enter fullscreen mode Exit fullscreen mode

Now SQL becomes:

SELECT TOP(10) * FROM Users WHERE Age > 20
Enter fullscreen mode Exit fullscreen mode

Massive difference.

Same LINQ syntax.

Completely different execution model.


When IQueryable Becomes Dangerous

Not everything should be IQueryable.

Consider this:

public IQueryable<User> GetUsers()
{
    return _context.Users;
}
Enter fullscreen mode Exit fullscreen mode

Exposing IQueryable from repositories leaks:

  • Database provider details
  • Translation boundaries
  • Execution timing

It pushes data access responsibility upward.

In Clean Architecture, many teams prefer returning:

Task<List<User>> GetUsersAsync();
Enter fullscreen mode Exit fullscreen mode

To control execution at the infrastructure layer.

IQueryable is powerful.

But it leaks abstraction if misused.


IQueryable vs IEnumerable: A Mental Model

Think of it this way:

IEnumerable → "Run this algorithm here."\
IQueryable → "Describe this algorithm so someone else can run it."

One executes.

One describes.

That distinction is architectural gold.


Performance Breakdown

Concern IEnumerable IQueryable


Execution Location .NET Runtime Database Engine
Lambda Type Func<T,bool> Expression<Func<T,bool>>
Translation None SQL translation
Best For In-memory collections EF Core / ORM queries
Risk Over-fetching data Provider limitations


Advanced Insight: Provider Limitations

Expression trees must be translatable.

This will fail:

db.Users.Where(x => MyCustomMethod(x));
Enter fullscreen mode Exit fullscreen mode

Unless EF can translate MyCustomMethod.

IQueryable is not magic.

It is constrained by the provider.

That's why some queries throw runtime exceptions.

Because translation failed.


Choosing Correctly (2026 Rule Set)

✔ Use IQueryable while composing database queries.\
✔ Call ToList() only at the boundary.\
✔ Use IEnumerable after materialization.\
✔ Avoid exposing IQueryable from domain layers.\
✔ Be explicit when switching providers (AsEnumerable()).


The Day It Finally Makes Sense

The confusion disappears when you stop thinking about LINQ methods and
start thinking about execution engines.

IEnumerable is local execution.

IQueryable is remote execution planning.

Same syntax.

Different universe.

If you understand that boundary, you stop writing accidental performance
bugs.

And that is the moment you graduate from writing code...

...to designing execution.


Cristian Sifuentes\
Full-stack engineer · Performance-first mindset · .NET 2026

Top comments (0)