C# IEnumerable vs IQueryable: Key Differences
c# ienumerable vs iqueryable: Understand the practical differences between IEnumerable and IQueryable in C#: how they execute, when to use each, and how the choice aff...
The decision between IEnumerable and IQueryable in C# LINQ is not a style preference; it determines where and how your queries execute. Using the wrong one can cause a database to load thousands of rows into memory when only a few are needed. This article explains the c# ienumerable vs iqueryable distinction, how each type behaves, and the criteria that should guide your choice.
The core difference is about execution location and timing. IEnumerable represents an in-memory sequence that is iterated client-side. IQueryable represents a query that can be translated into an expression tree, which a provider such as Entity Framework can convert into SQL. That distinction has direct consequences for database performance, memory usage, and the operations you can perform.
Understanding IEnumerable and Its Execution Model
IEnumerable<T> is an interface that exposes a GetEnumerator() method. When you write a LINQ query over an IEnumerable<T>, the query is compiled into a series of method calls that operate on the in-memory collection. The query is not executed until you start iterating, a concept known as deferred execution.
IEnumerable<Customer> customers = GetCustomersFromMemory(); IEnumerable<Customer> vip = customers.Where(c => c.IsVip);
The Where operation does not run at the point it is written. It sets up an iterator that will filter as you enumerate. The filtering happens as you loop over vip:
foreach (var customer in vip) { Console.WriteLine(customer.Name); }
Each iteration pulls the next customer from the original in-memory list and applies the predicate. There is no translation to another language; all work occurs in the application process. This is efficient for small in-memory collections because there is no extra abstraction layer.
However, if the source is a database table, the picture changes. Suppose you write the same query against an IEnumerable<Customer> that wraps a database table. The framework cannot translate the predicate into SQL because IEnumerable only knows how to enumerate the sequence. The entire table must first be loaded into memory, then filtered locally. That defeats the purpose of database-side filtering and can be disastrous for large tables.
How IQueryable Supports Expression Tree Translation
IQueryable<T> also represents a sequence, but it carries an expression tree that describes the query. The Expression property holds a tree of nodes for each operation in the chain. A provider, such as Entity Framework Core's SQL Server provider, can inspect that tree and translate it into a native query language like SQL.
IQueryable<Customer> customers = _context.Customers; IQueryable<Customer> vip = customers.Where(c => c.IsVip);
Here, the Where method adds an expression node to the tree instead of compiling an in-memory delegate. When you eventually run the query, the provider converts the whole tree into a SQL statement and executes it on the database server.
This distinction becomes visible when you analyze the generated SQL. The following code fetches only VIP customers who live in a specific city, and the filtering happens in the database, not in application memory.
var vipInLondon = _context.Customers .Where(c => c.IsVip && c.City == "London") .ToList();
Assuming _context.Customers is IQueryable<Customer>, the database executes something equivalent to:
SELECT * FROM Customers WHERE IsVip = 1 AND City = 'London'
Only the matching rows cross the network. This is the practical advantage of IQueryable: it enables server-side filtering, sorting, and aggregation, which reduces memory pressure and network traffic.
Key Differences at a Glance
The following table summarizes the most meaningful differences you will encounter in everyday development.
| Aspect | IEnumerable<T> | IQueryable<T> |
|---|---|---|
| Execution location | In-memory on the client | Translated and executed by provider, often database |
| Query representation | Compiled delegate methods | Expression tree |
| Deferred execution | Yes, until enumeration | Yes, until query is executed |
| Typical data source | In-memory collections, arrays, | |
| lists | Database-backed stores via LINQ providers | |
| Supported operations | General LINQ operators, runs locally | Operators that the provider can translate |
| Performance implication | May load entire dataset into memory | Allows server-side filtering and projection |
This table highlights why you cannot blindly replace one with the other. Choosing IEnumerable over IQueryable in a database query will not just be slower; it will break the query translation and force a full table load.
Deferred Execution and When Queries Actually Run
Both interfaces exhibit deferred execution, meaning the query is not executed when you define it. The query runs when you enumerate it, typically with foreach, or when you call a terminal operator like ToList(), First(), Count(), or Single().
// No SQL is sent to the database yet. IQueryable<Order> query = _context.Orders.Where(o => o.Total > 100); // The database query executes here. var result = query.ToList();
This behavior is essential for building complex queries incrementally. You can add conditions based on user input without executing intermediate queries. With IQueryable, each Where extends the expression tree, and the final execution compiles the whole thing into one SQL statement.
For IEnumerable, deferred execution means the iterator is lazy, but each iteration step calls the local predicate. It does not defer the heavy lifting to a remote server.
Performance and Memory Implications
Performance is where the choice has the most visible impact. In an IQueryable scenario, filtering, sorting, and paging are pushed to the database. This reduces the amount of data transferred and the memory footprint of your application. For instance, pagination using Skip and Take:
var page = _context.Products .OrderBy(p => p.Name) .Skip(50) .Take(10) .ToList();
With IQueryable, the database returns only ten rows. With IEnumerable, the entire product table would be loaded into memory first, then skipped and taken client-side. For a catalog with millions of rows, that is an unacceptable overhead.
There is also a subtle performance cost with IQueryable itself. Expression tree compilation involves provider translation, which has overhead. For a small in-memory list, using IQueryable adds unnecessary abstraction and may reduce performance because you cannot use simple compiled delegates. In that case, IEnumerable is the right tool.
You should also consider the cost of multiple enumerations. An IEnumerable that wraps a database call will hit the database every time you enumerate it, unless you materialize it with ToList(). The same applies to IQueryable. If you need to iterate over the same result multiple times, materialize it into a list or array after fetching.
When to Use IEnumerable vs IQueryable
The decision depends on the data source and whether you need server-side processing.
Use IEnumerable<T> when:
- Your data already lives in memory, such as a
List<T>, an array, or aDictionary. - You are performing operations that cannot be translated to SQL, like calling a custom C# function inside a LINQ predicate on a database-backed query. In that case, you might first materialize, then apply the function in memory.
- The dataset is small and you do not need database-side filtering.
Use IQueryable<T> when:
- You are querying a database through a LINQ provider such as Entity Framework Core or LINQ to SQL.
- You need to filter, sort, group, or join data on the server to minimize what is transferred.
- You are building dynamic queries incrementally, because you can compose expression trees without executing intermediate steps.
A typical mistake is applying an IEnumerable method to an IQueryable chain. For example, calling AsEnumerable() on an IQueryable forces the remaining operations to execute in-memory. This can be intentional, for example when you need to use a custom C# method in a Where clause. However, it also ends the server-side translation. If you call AsEnumerable() early in the chain, later filters will run on the client, which may load unnecessary data.
Real-World Example: Filtering and Projection
Consider a customer search endpoint where the user can filter by name and city. With IQueryable, you can construct the query conditionally without hitting the database multiple times.
IQueryable<Customer> query = _context.Customers; if (!string.IsNullOrEmpty(nameFilter)) { query = query.Where(c => c.Name.Contains(nameFilter)); } if (!string.IsNullOrEmpty(cityFilter)) { query = query.Where(c => c.City == cityFilter); } var result = query.Select(c => new CustomerDto { Id = c.Id, Name = c.Name }).ToList();
Because query remains IQueryable, each Where adds to the expression tree. The final Select projects only the needed columns into SQL. The database performs the filtering and projection, and the application receives a lean result set.
If you accidentally declared query as IEnumerable<Customer>, this would not work the same way. The Where calls would operate on the in-memory collection, and the database would have to return the whole table. That is why the declared type matters as much as the underlying implementation.
Compatibility and Maintainability Constraints
IQueryable relies on the ability of the provider to translate your LINQ expression into a query language. Not every LINQ operator is translatable. For example, calling a custom method inside a Where predicate generally fails with a NotSupportedException in Entity Framework Core because the provider does not know how to convert that method to SQL. You must either rewrite the logic in a translatable form or materialize the data first.
Another maintainability concern is that IQueryable hides the execution context. A developer reading the code might not know whether the data is in memory or on a remote server. This can lead to subtle bugs if someone later adds an operation that forces client-side evaluation. For example, calling AsEnumerable() or ToList() mid-chain changes performance characteristics and potentially correctness if custom logic relies on server-side translation.
For in-memory collections, IEnumerable is the more predictable choice. It is a standard part of the .NET collection interfaces, and its behavior does not depend on a provider. This makes unit testing easier because you can pass a List<T> and assert on the results without mocking a database context.
Provider Translation Boundaries
Each LINQ provider defines its own translation capabilities. The same LINQ query that works on one provider might fail on another because of differences in expression tree handling. For example, string comparison behavior can vary between SQL Server and SQLite. You must be aware of the provider's limitations when designing complex queries.
A practical example is case sensitivity. With IQueryable, the translation depends on the database collation. With IEnumerable, the comparison is done by the .NET string comparison rules. The same Where(c => c.Name == "john") might return different results depending on whether it executes in SQL Server (case-insensitive by default) or in memory (case-sensitive by default). Understanding where execution occurs helps you predict such differences.
This is not merely a performance tradeoff; it is a correctness concern. If you need consistent case-insensitive matching across environments, you may need to normalize the data or adjust the query to match the provider's behavior. In such cases, you might choose to materialize with ToList() first, then apply a case-insensitive comparison using StringComparison.OrdinalIgnoreCase in memory.
Choosing the Right Type in Practice
The best practical guidance is to default to the natural interface for the data source. If you are receiving data from a database context, use IQueryable. If you are working with an in-memory list, use IEnumerable. When you design APIs, consider exposing these types deliberately.
If you expose IEnumerable<T> as a return type, you prevent the consumer from adding database-side operations later. Conversely, exposing IQueryable<T> allows callers to build on the query, but it also exposes the data source's translation capabilities, which might be a leaky abstraction. For a repository pattern, some teams prefer returning IQueryable to allow flexible querying, while others prefer IEnumerable to enforce boundaries and prevent accidental heavy queries.
A balanced approach is to return IQueryable only when you intend for the caller to compose the query, and IEnumerable or List when you have already executed the query and want to pass a concrete, in-memory result.
Reflecting on the Differences in a Real Query
To see the effect in action, consider a query that retrieves all customers from a database, then filters in memory. This often happens when a developer writes GetAllCustomers() returning a List<Customer> and then applies LINQ to that list. The database has already fetched every customer. The subsequent filtering, sorting, and paging all happen in memory. For a small lookup table, this is fine. For a growing table, it becomes a bottleneck.
A better approach is to push the filtering to the database. If your method returns IQueryable<Customer>, the caller can apply Where, OrderBy, and Take before executing the query. The database does the work, and the application receives only the required subset.
That is the fundamental difference between c# ienumerable vs iqueryable: one operates on the data you already have, while the other defines a query that can be optimized and executed elsewhere. Recognizing which one you are dealing with and how it affects execution is a core skill for writing efficient LINQ code.