Question

IQueryable vs IEnumerable in C#: Differences, Deferred Execution, and When to Use Each

csharplinqlinq-to-sqlienumerableiqueryable

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
  • Add .Where(c => c.City == "London")
  • Entity Framework translates that into SQL with a WHERE clause
  • The database does the filtering before sending results back

Why this matters

This difference affects:

  • Performance: pushing work to the database is often more efficient
  • Network usage: fewer rows may need to be transferred
  • Composability: callers can keep adding filters to an IQueryable<T>
  • Safety and design: exposing IQueryable<T> can leak data access details and make behavior harder to control

Are both deferred?

Usually, yes.

Both IEnumerable<T> and IQueryable<T> can be deferred, meaning the query does not execute until you enumerate it, such as with:

  • foreach
  • .ToList()
  • .ToArray()
  • .First()
  • .Count()

But deferred execution is not the key difference. The key difference is:

  • IEnumerable<T>: execute LINQ operations in memory
  • IQueryable<T>: build a provider-translatable query that may execute remotely, such as in SQL

Important subtlety

If you assign an IQueryable<T> source to an IEnumerable<T> variable, the underlying source may still be queryable. But once you use operators that bind to Enumerable instead of Queryable, the remaining operations happen in memory.

That means the declared type alone is not the whole story. The real question is whether the LINQ pipeline is being built as Queryable expressions or Enumerable delegates.

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 source
  • IEnumerable<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>.

Take Quiz

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:

  • query is 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:

  1. db.Customers.Where(...) builds a query the provider can translate
  2. ToList() sends the SQL query to the database
  3. After that, loaded.Where(...) runs in C# against in-memory objects

Example showing the design difference

public IQueryable<Customer> GetCustomersQuery()
{
    return db.Customers.Where(c => c.IsActive);
}

public IEnumerable<Customer> GetActiveCustomers()
{
    return db.Customers
        .Where(c => c.IsActive)
        .ToList();
}

The first method:

  • returns a composable query
  • lets the caller add more filters, sorting, or paging

The second method:

  • executes inside the method
  • returns a finished result set
  • hides the query provider from the caller

A useful boundary method

public List<CustomerDto> GetCustomersByCity(string city)
{
    return db.Customers
        .Where(c => c.City == city)
        .Select(c => new CustomerDto
        {
            Id = c.Id,
            Name = c.Name
        })
        .ToList();
}

This is common in applications because it:

  • controls what is selected
  • executes in one place
  • returns plain data rather than an open query

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);

Still no execution yet. The provider now has a query that means:

  • get customers where city is Paris
  • return only the Name column

4. foreach

foreach (var name in result)
{
    Console.WriteLine(name);
}

Now execution happens.

The provider may generate SQL similar to:

SELECT Name
FROM Customers
WHERE City = 'Paris'

The database returns the names, and the loop prints them.

What changes with IEnumerable<T>?

If you do this:

IEnumerable<Customer> customers = db.Customers.ToList();
var result = customers.Where(c => c.City == "Paris");

Then the steps are different:

  1. ToList() loads all customers into memory
  2. .Where(...) filters in your application
  3. the database is no longer involved in that filter

That is often less efficient if the original dataset is large.

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

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var even = numbers.Where(n => n % 2 == 0);

This is a normal in-memory LINQ query.

Returning a finished result from a service

A service may want to hide data access details and return already-prepared results:

public IEnumerable<CustomerDto> GetRecentCustomers()
{
    return db.Customers
        .OrderByDescending(c => c.CreatedAt)
        .Take(20)
        .Select(c => new CustomerDto { Id = c.Id, Name = c.Name })
        .ToList();
}

This avoids exposing query-building behavior to callers.

Take Quiz

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

if (string.IsNullOrWhiteSpace(city))
    return new List<CustomerDto>();

Projection before materialization

var items = db.Customers
    .Where(c => c.IsActive)
    .Select(c => new CustomerDto { Id = c.Id, Name = c.Name })
    .ToList();

This ensures only needed columns are fetched.

Early materialization when required

Sometimes you intentionally switch to in-memory processing:

var local = db.Customers
    .Where(c => c.IsActive)
    .ToList()
    .Where(c => CustomCSharpMethod(c));

This is useful when CustomCSharpMethod cannot be translated to SQL.

Practical rule

  • Use IQueryable<T> inside query-building code
  • Return materialized results or simpler abstractions from application/service boundaries unless you intentionally want to expose 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

public List<CustomerDto> GetActiveCustomers()
{
    return db.Customers
        .Where(c => c.IsActive)
        .Select(c => new CustomerDto { Id = c.Id, Name = c.Name })
        .ToList();
}

3. Materializing too early

Broken example:

var customers = db.Customers.ToList();
var london = customers.Where(c => c.City == "London");

Problem:

  • all rows are loaded first
  • filtering happens in memory

Better:

var london = db.Customers
    .Where(c => c.City == "London")
    .ToList();

4. Using non-translatable methods in an IQueryable<T> pipeline

Example:

var result = db.Customers.Where(c => IsPreferredCustomer(c));

If IsPreferredCustomer is a normal C# method, the provider may not be able to translate it to SQL.

Fix options

  • rewrite the condition with translatable expressions
  • or materialize first, then use the method in memory
var result = db.Customers
    .ToList()
    .Where(c => IsPreferredCustomer(c));

5. Deferred execution after context disposal

Broken example:

public IQueryable<Customer> GetCustomers()
{
    using var db = new AppDbContext();
    return db.Customers.Where(c => c.IsActive);
}

The returned query may execute after db has already been disposed.

Better:

public List<Customer> GetCustomers()
{
    using var db = new AppDbContext();
    return db.Customers.Where(c => c.IsActive).ToList();
}

Comparisons

ConceptIQueryable<T>IEnumerable<T>
Main purposeBuild provider-translatable queriesIterate over sequences in memory
LINQ operations useExpression treesDelegates
Typical sourceEntity Framework, LINQ providersArrays, lists, materialized results
Where filtering happensOften in database or remote providerIn application memory
Deferred executionUsually yesUsually yes
Good forQuery composition, filtering, paging before executionIteration and in-memory processing
RiskExposes data access details and delayed executionCan be inefficient if data is loaded too early

ToList() vs deferred query

ApproachWhat it means
db.Customers.Where(... )Build query, no execution yet
db.Customers.Where(...).ToList()Execute now and load results

Practical comparison

// Database-side filtering
var query = db.Customers.Where(c => c.City == "Rome");
// In-memory filtering after loading
var list = db.Customers.ToList();
var query = list.Where(c => c.City == "Rome");

The first usually translates to SQL. The second loads everything first and filters later.

Take Quiz

Cheat Sheet

Quick reference

  • IQueryable<T> = query provider can translate and optimize the query
  • IEnumerable<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

  • Compose as IQueryable<T>
  • Materialize with ToList() at the right boundary
  • Process as IEnumerable<T> after data is loaded

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.

Can I mix IQueryable<T> and IEnumerable<T> in one pipeline?

Yes. This often happens when part of the query runs in the database, then later processing runs in memory after materialization.

Take Quiz

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

  1. Start from a customer query source.
  2. Add an optional city filter only when a city value is provided.
  3. Filter to active customers.
  4. Project the result to customer names only.
  5. Execute the query and return a List<string>.
Take Quiz

Keep learning

Related questions

View more