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);
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);
At this point:
-
usersis already materialized in memory -
.Where()is an extension method fromSystem.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);
Notice the key detail:
Func<TSource, bool>
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));
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);
What happened?
-
db.Users.ToList()pulled every row from the database. - Filtering happened in memory.
- 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);
Notice what is different:
- No
ToList() -
db.UsersisIQueryable<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);
The key difference:
Expression<Func<TSource, bool>>
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
Is converted into an expression tree like:
BinaryExpression
Left: MemberExpression (x.Age)
Right: ConstantExpression (20)
Operator: GreaterThan
EF Core then translates this tree into SQL:
SELECT * FROM Users WHERE Age > 20
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
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));
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);
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();
Now SQL becomes:
SELECT TOP(10) * FROM Users WHERE Age > 20
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;
}
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();
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));
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)