IEnumerable<T> runs your query in memory. IQueryable<T> ships your query somewhere else to be run.
Every LINQ example so far has queried a plain List<Product> already sitting in memory. But what happens when your data lives somewhere else — a database table with a million rows? Pulling the entire table into memory just to filter it down to five rows with Where would be wasteful and slow. .NET solves this with a second interface, IQueryable<T>, that looks almost identical to IEnumerable<T> on the surface but behaves completely differently underneath.
This lesson is conceptual — it explains the difference so that when you meet IQueryable<T> later (fully, with Entity Framework Core, in Intermediate Part VI), you already understand why it exists. Every other lesson in this LINQ module stays entirely with IEnumerable<T> and in-memory data.
IEnumerable<T> means "this data is already here, in memory — walk through it item by item, running your logic directly in this process." Every LINQ operator you've used against a List<T> or array works this way: it's ordinary C# code, compiled and running right now, right here.
IQueryable<T> means "this data lives somewhere else — a database, a remote API. Don't run your filter here; instead, build a description of what you want, so it can be translated and sent to run there, close to the data." Only the final, already-filtered, already-narrowed result travels back to your application.
IEnumerable<T>'s LINQ operators (in System.Linq.Enumerable) take ordinary delegates — Func<T,bool> for Where, for example. A delegate is already-compiled executable code; there's no way to "read" what it does short of running it. So an IEnumerable<T> query can only ever run right here, in the current process, against data already sitting in memory.
IQueryable<T>'s LINQ operators (in System.Linq.Queryable) look identical at the call site, but instead of a delegate they take an Expression<Func<T,bool>> — an expression tree — a data structure that represents your lambda's logic as inspectable pieces (a comparison, a property access, a constant) rather than as compiled code. Because that tree can be inspected rather than just executed, a query provider (like Entity Framework Core's SQL provider) can walk it and translate it into another language entirely — SQL, most commonly — send that translated query to the actual data source, and bring back only the matching rows.
IEnumerable<T> operators receive a compiled delegate they can only run. IQueryable<T> operators receive an expression tree they can inspect and translate before anything runs at all. Everything else about how the two behave flows from that one distinction.
Imagine a database table of a million orders, and you want just this week's orders for one customer — maybe five rows. If querying that table only ever gave you an IEnumerable<Order>, the only way to run Where on it would be to first load all one million rows into your application's memory, and then filter down to five. That means pulling a million rows across the network, deserializing every one of them, just to throw 999,995 of them straight in the trash.
IQueryable<T> lets your .Where(o => o.CustomerId == 42 && o.Date >= startOfWeek) be turned into a WHERE CustomerId = 42 AND OrderDate >= @startOfWeek SQL clause, run by the database engine itself — which already has indexes and is built for exactly this. Only the five matching rows ever cross the network. The filtering intent is identical to LINQ to Objects; only where it executes, and how much data moves, changes.
.Where(...) compiled to a delegate.Where(...) compiled to an expression treedbContext.Orders.Where(o => o.CustomerId == 42) — this doesn't run anything yet.CustomerId to constant 42" — inspectable, not yet executable in the usual sense.Here's the same-looking query written against each kind of source, to show how identical the code looks at the call site — and how different what actually happens is.
// ─── IEnumerable<T> — LINQ to Objects (this module's entire focus) ───
List<Product> products = LoadAllProductsIntoMemory();
IEnumerable<Product> expensiveInMemory =
products.Where(p => p.Price > 100);
// The lambda is compiled to a delegate.
// Filtering happens item-by-item, right here, in this C# process.
// ─── IQueryable<T> — LINQ to Entities (conceptual preview only) ───
IQueryable<Product> catalog = dbContext.Products; // not loaded yet
IQueryable<Product> expensiveInDatabase =
catalog.Where(p => p.Price > 100);
// The lambda is compiled to an Expression<Func<Product, bool>>.
// Nothing has run yet — this just builds a query description.
var results = expensiveInDatabase.ToList();
// ONLY NOW does EF Core translate the tree into SQL,
// e.g. SELECT * FROM Products WHERE Price > 100,
// send it to the database, and bring back matching rows.Code → Meaning → Result: Both snippets read almost identically — that's deliberate; it's the whole point of LINQ being one unified vocabulary. But expensiveInMemory filters a list already sitting in your process's memory, while expensiveInDatabase builds a translatable query that isn't executed — anywhere — until you materialize it with something like ToList().
A typical ASP.NET Core web API endpoint for "get a customer's recent orders" looks roughly like this once EF Core is involved (this is conceptual context, not a lesson on EF Core itself):
[HttpGet("customers/{id}/orders")]
public async Task<IActionResult> GetRecentOrders(int id)
{
var recentOrders = await dbContext.Orders
.Where(o => o.CustomerId == id)
.Where(o => o.OrderDate >= DateTime.Today.AddDays(-30))
.OrderByDescending(o => o.OrderDate)
.ToListAsync(); // translated to SQL and executed here
return Ok(recentOrders);
}Both Where calls and the OrderByDescending get combined by EF Core into a single SQL statement with a WHERE and an ORDER BY clause — the database does the filtering and sorting using its indexes, and only the relevant handful of rows for that one customer, that one month, ever reach the API process. This is the entire reason IQueryable<T> exists: the same familiar LINQ syntax, but with the heavy lifting happening where the data already lives.
IEnumerable<T> is like reading a letter someone already handed you: the content is already in front of you, and you process it word by word, right where you're standing.
IQueryable<T> is like writing a letter of instructions and mailing it to a librarian in a distant archive: "please find me every book published after 2020 by this author." You don't send someone to bring back the entire library so you can search it yourself — you send a precise, translatable request, and only the matching books come back to you.
Func<Product, bool> — WHAT IEnumerable<T> RECEIVESp => p.Price > 100 is compiled straight to IL, wrapped in a delegate. You can invoke it. You cannot ask it "what comparison are you doing?" — it's opaque, already-compiled logic.Expression<Func<Product, bool>> — WHAT IQueryable<T> RECEIVESBinaryExpression node representing "greater than," with a MemberExpression child for p.Price and a ConstantExpression child for 100. This tree can be walked and read by ordinary code.IQueryable<T> and its operators are provider-agnostic — LINQ itself has no idea what SQL is. It's EF Core's specific query provider that walks the expression tree and knows how to turn "greater than a property" into SQL syntax. A different provider could translate the same tree into a completely different target language.IQueryable<T> isn't a performance optimization of the same in-memory mechanism — it's a fundamentally different execution model, where the query gets translated and shipped elsewhere. Since IQueryable<T> extends IEnumerable<T>, you can always enumerate one — but doing so triggers the translate-and-execute-remotely process, not an in-memory loop.
If you call a C# method inside an IQueryable<T> query that the provider has no SQL equivalent for (a custom C# helper method, for instance), one of two things happens: either you get a runtime exception saying the expression couldn't be translated, or — in older/looser configurations — the provider silently falls back to pulling more data into memory than you intended and filtering there. Either way, this is a genuine, well-known gotcha with real-world EF Core code, and it's exactly why understanding this distinction matters even before you write a line of EF Core.
.ToList() too early, then filtering in memory dbContext.Orders.ToList().Where(o => o.CustomerId == id) pulls the entire table into memory first, then filters in C# — defeating the entire purpose of IQueryable<T>. Keep the Where before ToList(), so it's part of the translated query: dbContext.Orders.Where(o => o.CustomerId == id).ToList().
Not every C# expression can be translated to SQL — complex custom logic, certain string methods, and arbitrary method calls may not have a translation. Keep IQueryable<T> query logic simple and translatable; do complex in-memory-only processing after materializing the results with ToList().
List<T>, array, or other in-memory collection.Include — are covered later, in Intermediate Part VI.List<T>.AsEnumerable()-style sources are IEnumerable<T>; a DbSet<T> from an EF Core DbContext is IQueryable<T>. Everywhere else in this module, assume IEnumerable<T> and in-memory data.
Where, Select, OrderBy — completely different execution underneath.IEnumerable<T>; full IQueryable<T>/EF Core mechanics come in Intermediate Part VI.
IEnumerable<T> queries run in-process, against data already in memory, using compiled delegates.IQueryable<T> queries build an expression tree describing what you want, which a provider translates and sends to run at the data source — most commonly, a database as SQL.ToList() too early on an IQueryable<T> forfeits translation and pulls everything into memory first — a real and common performance bug.IEnumerable<T> / LINQ to Objects — the full IQueryable<T> and EF Core story is a later module.You've seen how IEnumerable<T> and IQueryable<T> differ conceptually. Let's check your understanding.
1. What is the fundamental technical difference between how IEnumerable<T> and IQueryable<T> LINQ operators receive a lambda like p => p.Price > 100?
Correct: B
Why B is correct: This is the core distinction the whole lesson is built on — a delegate can only be run; an expression tree can be inspected and translated before it runs anywhere.
Why A is incorrect: The two really do compile to different underlying representations — that's precisely what makes translation to SQL (or any other target) possible for IQueryable<T> and impossible for IEnumerable<T>.
Why C is incorrect: This reverses the actual mapping.
Why D is incorrect: Neither receives raw SQL directly — SQL is only produced later, by the provider, from the expression tree.
Reinforcement: Delegate = opaque, run-only code. Expression tree = inspectable data describing code. That's the whole story.
2. Why does querying a huge database table through IQueryable<T> avoid pulling every row into memory, while doing the same filter on an in-memory IEnumerable<T> would require the data to already be there?
Correct: B
Why B is correct: This is exactly the "Why Does It Exist?" problem/solution from this lesson — translating the query to run at the source avoids transferring irrelevant data at all.
Why A is incorrect: It's not about raw hardware speed — it's about where the filtering computation happens and how much data crosses the network.
Why C is incorrect: IEnumerable<T> isn't deprecated at all — it's the correct, standard choice for in-memory data, which is most of what this module covers.
Why D is incorrect: The amount of data transferred is exactly what differs — that's the entire motivating problem for IQueryable<T>'s existence.
Reinforcement: Less data crossing the wire is the direct, practical payoff of query translation.
3. A developer writes dbContext.Orders.ToList().Where(o => o.CustomerId == id) against an EF Core DbSet<Order> (an IQueryable<Order>). What's the problem with this code?
Correct: B
Why B is correct: As covered in Common Mistakes, this is one of the most common real-world IQueryable<T> performance bugs — ToList() forces immediate materialization of everything up to that point, so any Where that comes after it runs in memory, on the full dataset, not translated to SQL at all.
Why A is incorrect: Position matters enormously for IQueryable<T> — operators chained before materialization get translated; operators chained after run in-memory as ordinary IEnumerable<T> LINQ.
Why C is incorrect: It compiles fine — List<T> supports Where too, via IEnumerable<T>'s LINQ to Objects operators — it's just semantically wasteful here, not illegal.
Why D is incorrect: IQueryable<T> extends IEnumerable<T> and supports ToList() too — that's precisely what triggers materialization.
Reinforcement: Keep filtering operators before materialization calls like ToList() so they get translated and run at the source.
4. Which statement best describes the relationship between LINQ, IEnumerable<T>, and IQueryable<T>?
Correct: B
Why B is correct: LINQ to Objects (System.Linq.Enumerable) targets IEnumerable<T>; LINQ to Entities/providers (System.Linq.Queryable) target IQueryable<T> — same method names, different execution underneath, as this whole lesson explained.
Why A is incorrect: It's the reverse — IEnumerable<T> LINQ to Objects is the one this entire module is built on; IQueryable<T> is the specialized extension for translatable sources.
Why C is incorrect: IQueryable<T> actually extends IEnumerable<T> — every IQueryable<T> is also an IEnumerable<T>, which is why you can still foreach over query results directly.
Why D is incorrect: As established back in Foundations, List<T>, dictionaries, and virtually every collection type implement IEnumerable<T> — it's not array-specific at all.
Reinforcement: Same vocabulary, two execution models — that's the single idea to carry out of this lesson.
You now understand the conceptual difference between IEnumerable<T> and IQueryable<T>. From here on, this module stays entirely in LINQ to Objects territory — starting with filtering.
dotnetmadeeasy.com — Learn C# and .NET, the right way.