Question
IQueryable vs IEnumerable in C#: Differences, Deferred Execution, and When to Use Each
Question
In C#, what is the difference between returning IQueryable<T> and IEnumerable<T> from a query, and when should each one be preferred?
For example:
IQueryable<Customer> custs =
from c in db.Customers
where c.City == "<City>"
select c;
IEnumerable<Customer> custs =
from c in db.Customers
where c.City == "<City>"
select c;
Will both versions use deferred execution? Also, in real applications, when is it better to return IQueryable<T> instead of IEnumerable<T>, or the other way around?
Short Answer
By the end of this page, you will understand the practical difference between IQueryable<T> and IEnumerable<T> in C#, how deferred execution applies to both, and why the main distinction is where the query runs and who gets to keep composing it. You will also learn when returning IQueryable<T> is useful, when it is risky, and why many codebases prefer materialized results or IEnumerable<T> at application boundaries.
Concept
IEnumerable<T> and IQueryable<T> can both represent sequences of data, and both can participate in deferred execution. However, they are designed for different kinds of querying.
IEnumerable<T>
IEnumerable<T> is the basic interface for iterating over a sequence in .NET.
- It works with in-memory collections like arrays, lists, and results already loaded from a database.
- LINQ operations on
IEnumerable<T>use delegates and run in .NET code. - Once data is in memory, filtering, sorting, and projection happen on the application side.
Example idea:
- Load 1,000 customers into memory
- Then filter with
.Where(c => c.City == "London") - The filtering happens in your C# process
IQueryable<T>
IQueryable<T> is meant for query providers that can translate LINQ expressions into another query language, such as SQL.
- It works with sources like Entity Framework or LINQ to SQL
- LINQ operations build an expression tree, not just executable delegates
- The provider can translate the query into SQL and run it in the database
Example idea:
- Start from
db.Customers
Mental Model
Think of IQueryable<T> as a restaurant order form and IEnumerable<T> as the actual meal on your table.
- With
IQueryable<T>, you are still writing instructions: "no onions," "extra cheese," "make it spicy." The kitchen can still change how the food is prepared. - With
IEnumerable<T>, the food is already in front of you. You can sort it, pick parts out, or rearrange it, but the kitchen is no longer involved.
Another way to think about it:
IQueryable<T>= a query plan that can still be translated and optimized by the data sourceIEnumerable<T>= a sequence you iterate over in your application
So if you want the database to do the filtering, grouping, or paging, keep the query as IQueryable<T> until the right moment. If you want a stable, already-fetched set of results, materialize it and work with IEnumerable<T> or a concrete collection like List<T>.
Syntax and Examples
Core syntax
IQueryable<Customer> query = db.Customers
.Where(c => c.City == "London")
.OrderBy(c => c.Name);
IEnumerable<Customer> customers = query.ToList();
In the first example:
queryis still a database-side query description- nothing is fetched yet
In the second example:
.ToList()executes the query- the results are loaded into memory
- the result can now be treated as
IEnumerable<Customer>
Example: database filtering vs in-memory filtering
// IQueryable: can be translated to SQL
IQueryable<Customer> query = db.Customers.Where(c => c.City == "London");
// Executes SQL here
List<Customer> loaded = query.ToList();
// IEnumerable: now filtering in memory
IEnumerable<Customer> filteredAgain = loaded.Where(c => c.Name.StartsWith("A"));
Explanation:
db.Customers.Where(...)builds a query the provider can translateToList()sends the SQL query to the database- After that, runs in C# against in-memory objects
Step by Step Execution
Consider this code:
var query = db.Customers.Where(c => c.City == "Paris");
var result = query.Select(c => c.Name);
foreach (var name in result)
{
Console.WriteLine(name);
}
Step by step
1. db.Customers
var customers = db.Customers;
This is usually an IQueryable<Customer> provided by Entity Framework or another LINQ provider.
2. .Where(c => c.City == "Paris")
var query = db.Customers.Where(c => c.City == "Paris");
This does not usually fetch rows immediately.
Instead, it creates a bigger query expression, roughly meaning:
- source: customers table
- condition: city equals Paris
3. .Select(c => c.Name)
var result = query.Select(c => c.Name);
Real World Use Cases
When IQueryable<T> is useful
Search pages with optional filters
A search screen may allow filtering by:
- city
- status
- created date
- name prefix
You can keep composing the query before execution:
IQueryable<Customer> query = db.Customers;
if (!string.IsNullOrWhiteSpace(city))
query = query.Where(c => c.City == city);
if (isActive.HasValue)
query = query.Where(c => c.IsActive == isActive.Value);
var results = query.ToList();
This is efficient because only the final filtered query is sent to the database.
Paging
var page = db.Customers
.Where(c => c.IsActive)
.OrderBy(c => c.Name)
.Skip(20)
.Take(10)
.ToList();
With IQueryable<T>, Skip and Take can become SQL paging instead of loading everything first.
When IEnumerable<T> is useful
Working with in-memory data
numbers = List<> { , , , , };
even = numbers.Where(n => n % == );
Real Codebase Usage
In real projects, developers often follow this pattern:
Inside the data access layer: build with IQueryable<T>
Repositories or query services may start with an IQueryable<T> so filters can be added progressively.
IQueryable<Customer> query = db.Customers;
if (onlyActive)
query = query.Where(c => c.IsActive);
if (!string.IsNullOrEmpty(city))
query = query.Where(c => c.City == city);
This works well for:
- optional filters
- sorting
- paging
- projections
At the application boundary: materialize results
Many codebases do not return IQueryable<T> from public service methods.
Instead, they execute the query and return:
List<T>IReadOnlyList<T>IEnumerable<T>- DTOs
Why?
- it prevents callers from accidentally changing the query
- it avoids leaking ORM details
- it makes performance easier to reason about
- it prevents query execution after the data context has been disposed
Common patterns
Guard clauses before query composition
Common Mistakes
1. Assuming the type alone decides everything
This is misleading:
IEnumerable<Customer> customers = db.Customers.Where(c => c.City == "Paris");
A beginner may think this guarantees in-memory behavior. Not necessarily. The underlying source may still be queryable until enumeration or until later operators switch to Enumerable behavior.
Better mindset
Ask:
- What is the source?
- Has the query been materialized with
ToList(),ToArray(), or similar? - Are later operators still translatable to the provider?
2. Returning IQueryable<T> from every repository method
Broken design example:
public IQueryable<Customer> GetCustomers()
{
return db.Customers;
}
Why this can be a problem:
- callers can build arbitrary queries
- performance becomes unpredictable
- the data layer leaks into the rest of the app
- the query may execute after the context is disposed
Safer alternative
List<CustomerDto> ()
{
db.Customers
.Where(c => c.IsActive)
.Select(c => CustomerDto { Id = c.Id, Name = c.Name })
.ToList();
}
Comparisons
| Concept | IQueryable<T> | IEnumerable<T> |
|---|---|---|
| Main purpose | Build provider-translatable queries | Iterate over sequences in memory |
| LINQ operations use | Expression trees | Delegates |
| Typical source | Entity Framework, LINQ providers | Arrays, lists, materialized results |
| Where filtering happens | Often in database or remote provider | In application memory |
| Deferred execution | Usually yes | Usually yes |
| Good for | Query composition, filtering, paging before execution | Iteration and in-memory processing |
| Risk | Exposes data access details and delayed execution | Can be inefficient if data is loaded too early |
Cheat Sheet
Quick reference
IQueryable<T>= query provider can translate and optimize the queryIEnumerable<T>= iterate over a sequence, usually in memory- Both often support deferred execution
- Deferred execution means the query runs only when enumerated
Common execution triggers
foreach (var item in query) { }
query.ToList();
query.ToArray();
query.First();
query.Count();
Prefer IQueryable<T> when
- you are still building a database query
- you need filtering, sorting, paging, projection before execution
- you are inside the data/query layer
Prefer IEnumerable<T> or materialized results when
- data is already in memory
- you want to return a completed result
- you want to hide ORM/query provider details
- you want predictable execution timing
Warning signs
- calling
ToList()too early - returning
IQueryable<T>from broad public APIs without a clear reason - using custom C# methods inside provider-translated queries
- enumerating a deferred query after the DbContext is disposed
Useful rule of thumb
FAQ
Does IQueryable<T> always mean SQL will be generated?
Not always. It means a provider can try to translate the query. In many .NET apps that provider is Entity Framework, which often generates SQL.
Does IEnumerable<T> always mean the query runs immediately?
No. IEnumerable<T> can also be deferred. Execution usually happens when you enumerate it.
Which is faster: IQueryable<T> or IEnumerable<T>?
Neither is inherently always faster. IQueryable<T> is usually better when filtering can be pushed to the database. IEnumerable<T> is appropriate once data is already in memory.
Should repositories return IQueryable<T>?
Sometimes, but many teams avoid exposing it broadly because it leaks query behavior and can make execution timing and performance harder to control.
What does ToList() do in this context?
It executes the query and materializes the results into a List<T>.
Why can returning IQueryable<T> be dangerous?
Because the caller can keep changing the query, execution is delayed, and the query may fail later if the underlying context has been disposed.
Mini Project
Description
Build a small customer search feature that demonstrates the practical difference between composing a query and executing it. The project will let a caller filter active customers by city and then return a finished list of names. This mirrors a common pattern in business applications where a query is built with optional conditions and only executed once.
Goal
Create a method that composes a customer query efficiently and returns a materialized result list.
Requirements
- Start from a customer query source.
- Add an optional city filter only when a city value is provided.
- Filter to active customers.
- Project the result to customer names only.
- Execute the query and return a
List<string>.
Keep learning
Related questions
AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection
Learn the differences between AddTransient, AddScoped, and AddSingleton in ASP.NET Core DI with examples and practical usage.
C# Type Checking Explained: typeof vs GetType() vs is
Learn when to use typeof, GetType(), and is in C#. Understand exact type checks, inheritance, and safe type testing clearly.
C# Version Numbers Explained: C# vs .NET Framework and Why “C# 3.5” Is Incorrect
Learn the correct C# version numbers, how they map to .NET releases, and why terms like C# 3.5 are inaccurate and confusing.