← Open in the full interactive course (progress tracking, search & more)

Where and Select mean two completely different things depending on one silent fact: the compile-time type of what you're calling them on.

Intermediate lesson 110 gave you the conceptual shape of IQueryable<T>: "it builds an expression tree instead of running a delegate, so a provider can translate it." That was deliberately a first look — enough to use EF Core sensibly without misunderstanding it, but stopping well short of the actual interface and the actual mechanism that makes the translation possible at all. Now that you've spent lesson 196 inside IEnumerable<T>'s real machinery, and lesson 188/189 inside expression trees and lambda compilation, you have everything needed to open IQueryable<T> up completely.

You'll see the exact interface — Expression, ElementType, Provider — and the single most important fact about LINQ that most developers never learn explicitly: System.Linq.Queryable and System.Linq.Enumerable define methods with the exact same names but completely different bodies, and the compiler silently picks between them based on nothing but the compile-time type of your sequence.

What Is It?

The Simple Explanation

IQueryable<T> is an IEnumerable<T> that also carries a self-description: the expression tree that built it so far, and a reference to the object that knows how to actually run that tree against a real data source. Every .Where(...), .OrderBy(...), or .Select(...) call you chain onto it doesn't filter, sort, or project anything — it hands back a new IQueryable<T> wrapping a slightly bigger expression tree than the one before it.

The Technical Definition — the Exact Interface

public interface IQueryable<out T> : IEnumerable<T>, IQueryable, IEnumerable { // Inherited from the non-generic IQueryable: // Type ElementType { get; } — the runtime element type (typeof(T)) // Expression Expression { get; } — the expression tree built SO FAR // IQueryProvider Provider { get; } — the object that knows how to RUN that tree } public interface IQueryProvider { IQueryable CreateQuery(Expression expression); IQueryable<TElement> CreateQuery<TElement>(Expression expression); object Execute(Expression expression); TResult Execute<TResult>(Expression expression); }

Three members beyond what IEnumerable<T> already gave you: ElementType (rarely used directly — mostly for reflection-heavy generic scenarios), Expression (the tree — everything you've built by chaining operators, all the way back to the original source), and Provider — the piece that turns that tree into an actual result. IQueryProvider's CreateQuery builds a new IQueryable<T> from an expression tree without running anything (this is what every Queryable operator calls internally); Execute actually runs a tree and hands back a real value or sequence (this is what enumeration — or a scalar-returning call like Count() — ultimately triggers).

The Key Fact This Lesson Is Built On

A concrete IQueryable<T> implementation — like EF Core's DbSet<T> — is really just a small wrapper carrying two things: an Expression tree that describes "everything asked for so far," and a Provider reference back to whatever knows how to turn that tree into SQL (or any other target). Every operator you chain builds a bigger tree and a new wrapper around it. Nothing runs until Provider.Execute is called — and that's the entire mechanism this lesson exists to make concrete.

Why Does It Exist?

The Problem — LINQ's Method Names Are Already Taken

By the time IQueryable<T> was designed, System.Linq.Enumerable already owned Where, Select, OrderBy, and dozens of other extension methods on IEnumerable<T> — every one of them taking a plain delegate and running it immediately, item by item, in-process. That's the entire mechanism lesson 196 dissected. A translatable, provider-driven query needs the exact same vocabulary — the same familiar Where, Select, OrderBy — but it categorically cannot use Enumerable's implementations, because those implementations invoke a compiled delegate directly, and a compiled delegate's logic cannot be inspected or turned into SQL after the fact (the exact limitation lesson 188 covered in depth).

The Solution — a Second, Parallel Set of Methods, Resolved by Compile-Time Type

.NET's answer is System.Linq.Queryable — a second static class, with extension methods bearing the exact same names, extending IQueryable<T> instead of IEnumerable<T>, and accepting Expression<Func<T,bool>> instead of Func<T,bool>. Because C#'s overload resolution is driven entirely by the compile-time type of the expression you're calling the method on, the exact same-looking line of code — source.Where(x => x.Price > 100) — silently resolves to a completely different method depending on whether source is typed as IEnumerable<T> or IQueryable<T>. No runtime check, no branching, no reflection — it's decided once, at compile time, and baked permanently into the emitted IL.

Big Picture

TWO CLASSES, SAME METHOD NAMES, COMPLETELY DIFFERENT BODIES
System.Linq.Enumerable
Extends IEnumerable<T>
Parameter type: Func<T,bool>
Body: a yield return loop (lesson 196) that runs your delegate, item by item
System.Linq.Queryable
Extends IQueryable<T>
Parameter type: Expression<Func<T,bool>>
Body: wraps the tree onto source.Expression and calls Provider.CreateQueryruns nothing
Same call site. Same method name. The compile-time type of the receiver decides which class answers.

How It Works

Here is what Queryable.Where actually looks like — genuinely, not a simplification of some hidden complexity. The real implementation in the BCL does almost exactly this:

// The REAL shape of Queryable.Where (simplified only in variable names): public static IQueryable<T> Where<T>( this IQueryable<T> source, Expression<Func<T, bool>> predicate) { return source.Provider.CreateQuery<T>( Expression.Call( null, // the MethodInfo for THIS SAME Where<T> method, via reflection GetMethodInfo(Where, source, predicate), source.Expression, // the tree built so far Expression.Quote(predicate) // your lambda, wrapped as another tree node ) ); }

Notice what's genuinely absent from this method: there is no loop. There is no call to predicate(...) anywhere. Queryable.Where never touches a single element of T — it builds one new MethodCallExpression node (representing "a call to Where, with these arguments"), attaches your predicate's own expression tree as a child of it, appends the whole thing onto source.Expression, and asks the provider to wrap the result in a new IQueryable<T>. That's the entire method.

A CHAINED QUERY, STEP BY STEP
1. dbSet.Where(...)
2. .OrderBy(...) — chained onto the RESULT of step 1
3. .Select(...) — chained again
4. ONLY NOW — enumeration (foreach, ToList(), await ...ToListAsync()) — TRIGGERS Provider.Execute

Simple Example

You can watch the tree grow, one call at a time, just by printing .Expression — it has a readable, if verbose, string representation:

IQueryable<Product> step0 = context.Products; // an IQueryable<Product>, backed by EF Core Console.WriteLine(step0.Expression); // DbSet<Product>() IQueryable<Product> step1 = step0.Where(p => p.Price > 100); Console.WriteLine(step1.Expression); // Where(DbSet<Product>(), p => (p.Price > 100)) // ↑ step0's ENTIRE tree, now wrapped in a new "Where" call node IQueryable<Product> step2 = step1.OrderBy(p => p.Name); Console.WriteLine(step2.Expression); // OrderBy(Where(DbSet<Product>(), p => (p.Price > 100)), p => p.Name) // ↑ step1's tree, now wrapped AGAIN — one single, ever-growing tree // Nothing above touched the database. This line does: List<Product> results = step2.ToList(); // ONLY NOW: step2.Provider.Execute(step2.Expression) runs, translates // the WHOLE tree to one SQL statement, and materializes the results.

Code → Meaning → Result: Each variable — step0, step1, step2 — is a fully independent, immutable IQueryable<T>, each wrapping its own progressively larger expression tree. None of them share mutable state, and building three of them costs nothing but three small allocations — no query, no connection, no SQL, until .ToList() is finally called on the last one.

Real-World Example

Recall this exact query from Intermediate lesson 145 — you now have the complete, mechanical story behind it:

List<Product> products = await context.Products .Where(p => p.Price > 20) .OrderBy(p => p.Name) .ToListAsync();

Trace it fully, one call at a time:

Every part of this was already familiar from lesson 145 as a black box labeled "translation happens." Now you can point at the exact three interface members — Expression, Provider, and the CreateQuery/Execute pair — that make it happen, and the exact static class (Queryable, not Enumerable) whose method bodies are responsible.

Analogy

Amending a Legal Document vs Actually Doing the Thing

Imagine a legal contract being amended clause by clause: "add a clause requiring X," "add a clause requiring Y." Each amendment produces a new, complete version of the document — nobody has actually gone and done X or Y yet; the document simply now describes a larger set of obligations than before. Queryable.Where, .OrderBy, and .Select are exactly this: each one hands back a new, complete "document" (the expression tree) describing a slightly bigger set of instructions than the one before it. Only when someone finally takes the finished document to the courthouse to be executed — Provider.Execute, triggered by enumeration — does anything in the real world actually happen, and it happens all at once, based on the document's final, complete form.

Under the Hood

HOW OVERLOAD RESOLUTION SILENTLY PICKS THE RIGHT CLASS
1. IT'S ORDINARY C# EXTENSION METHOD RESOLUTION — NOTHING SPECIAL TO LINQ
2. THE LAMBDA'S COMPILATION FOLLOWS THE PARAMETER TYPE, NOT THE OTHER WAY AROUND
3. THE "RECEIVER TYPE" IS THE COMPILE-TIME TYPE — NOT THE RUNTIME TYPE
4. Expression.Quote — WHY YOUR LAMBDA APPEARS "WRAPPED" INSIDE THE OUTER TREE

Common Confusion

1. "There's one Where method that behaves differently for different sources" — no, there are genuinely two separate methods

Enumerable.Where and Queryable.Where are two entirely distinct methods, in two distinct static classes, with two distinct bodies compiled from two distinct source files inside the BCL. They happen to share a name and a broadly similar signature shape, which is precisely the illusion this lesson is built to dispel — at the call site they're indistinguishable; underneath, one loops and invokes a delegate (lesson 196), the other builds tree nodes and calls a provider.

2. Chaining .Where().OrderBy().Select() does not run three separate round trips, or even three separate steps at translation time

Each call produces a new IQueryable<T> with a progressively bigger single tree — but the provider doesn't translate Where, then translate OrderBy, then translate Select, stitching results together. It receives the entire final tree in one call to Execute and analyzes the whole thing holistically, which is exactly how it produces one clean SQL statement with a WHERE, an ORDER BY, and a column list, rather than three separate queries chained together.

Common Mistakes

Mistake 1 — Declaring a parameter as IEnumerable<T> when the real source is an EF Core IQueryable<T>

A method that quietly widens the type it accepts:

// Accepts IEnumerable<T> — silently forces Enumerable.Where, no matter what's passed in List<Product> GetExpensive(IEnumerable<Product> source, decimal min) => source.Where(p => p.Price > min).ToList(); // Caller passes an EF Core DbSet<Product> — a real IQueryable<Product> at runtime var expensive = GetExpensive(context.Products, 100m); // Inside GetExpensive, "source" is COMPILE-TIME typed as IEnumerable<Product>. // .Where(...) resolves to Enumerable.Where — which means EF Core must first // materialize the ENTIRE table into memory (to satisfy IEnumerable<T>'s contract) // before the filter runs, in-process, on every single row.

Accept IQueryable<T> when the caller may be passing a translatable source, so .Where(...) resolves to Queryable.Where and stays translatable all the way to the database:

// Accepts IQueryable<T> — Where resolves to Queryable.Where, stays translatable List<Product> GetExpensive(IQueryable<Product> source, decimal min) => source.Where(p => p.Price > min).ToList();

Mistake 2 — Assuming Provider.Execute runs once per chained operator

Believing that .Where(...).OrderBy(...).Select(...) triggers the provider three separate times, once per call — leading to worrying about "three queries" that don't actually happen. Understand that each intermediate call only builds a bigger Expression and returns a new IQueryable<T> wrapper — Provider.Execute (or CreateQuery, for the intermediate steps, which still doesn't run anything) is called only when something finally enumerates the result.

When Should I Use It?

Reason about the exact IQueryable<T> mechanism when

You still don't implement IQueryable<T> or IQueryProvider yourself

Rule of thumb: When a method parameter's only job is to be enumerated once, in-memory, IEnumerable<T> is simplest. When that parameter might come from EF Core (or any translatable source) and you want filtering/sorting/paging to stay translatable all the way to the query, type it as IQueryable<T> instead — the difference is which static class answers your LINQ calls.

Mental Model

IQueryable<T> = "an ever-growing expression tree, plus a reference to whoever can run it"
Every Queryable operator call = "wrap the tree a little bigger, hand back a new immutable wrapper — run nothing"
Enumeration (foreach, ToList, Count) = "the ONE moment Provider.Execute is finally called, on the whole tree, at once"

Remember:
· Enumerable.Where and Queryable.Where are two different methods, chosen by the compile-time type of your sequence — never a runtime check.
· .Expression is one single, cumulative tree — chaining doesn't create separate trees per call.
· .Provider is the object that actually knows how to run the tree — LINQ's own operators never do that work themselves.
· Typing a parameter IEnumerable<T> instead of IQueryable<T> silently forces the wrong Where — a real, easy-to-miss bug.

Key Takeaway


Check Your Understanding

You've traced exactly how IQueryable<T> builds and defers a query. Let's check your understanding.

1. Beyond what IEnumerable<T> already provides, what three members does IQueryable<T> add?

Show answer

Correct: B

Why B is correct: As shown in the exact interface definition, IQueryable<T> adds Expression (the tree built so far), ElementType, and Provider (the IQueryProvider that can actually run the tree) on top of everything IEnumerable<T> already contributes.

Why A is incorrect: Those are LINQ extension methods usable on any sequence, not members of the IQueryable<T> interface itself.

Why C is incorrect: Those are ADO.NET-level concepts EF Core's provider works with internally — they aren't part of the provider-agnostic IQueryable<T>/IQueryProvider contract itself.

Why D is incorrect: Those belong to IEnumerable<T>/IEnumerator<T>, covered fully in lesson 196 — IQueryable<T> inherits them but they aren't the members it specifically adds.

Reinforcement: Expression + Provider is the whole mechanism — everything else in this lesson explains what those two members are used for.

2. What determines whether source.Where(x => x.Price > 100) resolves to Enumerable.Where or Queryable.Where?

Show answer

Correct: B

Why B is correct: As Under the Hood explained, this is ordinary C# extension method overload resolution, resolved entirely at compile time based on the declared type of source — it's baked into the emitted IL before the program ever runs.

Why A is incorrect: This is exactly the Mistake 1 trap — a variable declared IEnumerable<T> resolves to Enumerable.Where even when it holds a real DbSet<T> (an IQueryable<T>) at runtime.

Why C is incorrect: Whether EF Core is referenced has no bearing on which overload the compiler selects — that decision depends only on the declared type in your own code.

Why D is incorrect: There is no such runtime dispatch — Enumerable.Where and Queryable.Where are two entirely separate compiled methods; only one of them is ever called, decided at compile time.

Reinforcement: Compile-time type, not runtime type, decides which of the two identically-named methods answers the call — every time, with no exceptions.

3. After writing var q = context.Products.Where(p => p.Price > 100).OrderBy(p => p.Name); and nothing else, what has actually happened against the database?

Show answer

Correct: C

Why C is correct: As shown throughout How It Works and the Simple Example, each chained Queryable operator only builds a new, bigger Expression and wraps it via Provider.CreateQuery — no execution happens until something enumerates q.

Why A is incorrect: Neither operator individually triggers execution — this misconception is directly addressed in Common Confusion point 2.

Why B is incorrect: No SQL has been generated or sent yet at all — that only happens once Provider.Execute is invoked, which chaining alone never does.

Why D is incorrect: Building an expression tree doesn't require or trigger opening a database connection — that's part of the eventual execution step, not query construction.

Reinforcement: Building an IQueryable<T> chain is purely in-memory tree construction — completely inert until enumerated.

4. A method is declared List<Product> Filter(IEnumerable<Product> source, decimal min) => source.Where(p => p.Price > min).ToList(); and is called with an EF Core DbSet<Product> as the argument. What actually happens?

Show answer

Correct: B

Why B is correct: This is exactly Mistake 1 from this lesson. DbSet<Product> genuinely does implement IEnumerable<Product>, so the call compiles and runs fine — but inside Filter, source's compile-time type is IEnumerable<Product>, so Where resolves to Enumerable.Where, forcing full materialization before filtering.

Why A is incorrect: DbSet<Product> genuinely satisfies IEnumerable<Product> — this compiles without any error, which is precisely what makes the mistake so easy to miss.

Why C is incorrect: There is no runtime "upgrade" mechanism — overload resolution is fixed at compile time based purely on the declared parameter type, as question 2 established.

Why D is incorrect: The runtime type of the object passed in is irrelevant to which Where overload gets called inside Filter — only the parameter's declared type matters there.

Reinforcement: Type a parameter IQueryable<T>, not just IEnumerable<T>, whenever you want LINQ chained on it to stay translatable to the database.

You now know exactly what's inside every EF Core query you write. Next: how a provider like EF Core actually reads that tree — by walking it, one node at a time.


dotnetmadeeasy.com — Learn C# and .NET, the right way.