LINQ Internals (IQueryable vs. IEnumerable, Deferred Execution)
Mind Map Summary
- LINQ (Language-Integrated Query)
- Core Principle: Deferred Execution
- Definition: The query is built but not executed until the collection is enumerated.
- Methods that trigger execution:
ToList(),ToArray(),First(),Count(), or aforeachloop.
- Core Principle: Deferred Execution
- Key Interfaces
IEnumerable<T>(In-Memory)- Represents: A sequence of objects in memory.
- Usage: Querying local collections (e.g.,
List<T>). - Pitfall: In DB contexts, it fetches all data first, then filters in RAM.
IQueryable<T>(Remote)- Represents: A query to be executed by a remote provider.
- Usage: Querying databases via EF Core.
- Benefit: Translates logic into an Expression Tree, enabling SQL-side filtering.
Core Concepts
1. Deferred Execution
Deferred execution means that the evaluation of an expression is delayed until its realized value is actually required. In LINQ, this allows you to layer multiple filters, sorts, and projects without executing the query multiple times. You are essentially building a declarative “query plan” that is only enacted at the very last moment.
2. IEnumerable<T> vs. IQueryable<T>
The choice between these two interfaces is the most common source of performance bugs in .NET database applications.
IEnumerable<T>uses delegates (Func<T, bool>). To execute a delegate, the data must already be in the same process. Thus, EF Core must download your entire table before it can run your filter.IQueryable<T>uses expressions (Expression<Func<T, bool>>). These are data structures that EF Core can “read” and translate into SQLWHERE,JOIN, andSELECTclauses. This allows the database server—which is designed for this exact purpose—to do the heavy lifting.
Practice Exercise
Using EF Core, write a query against a DbSet<Product>. First, use .AsEnumerable() before a .Where() clause and inspect the behavior. Then, write the same logic using .AsQueryable() and explain the difference.
Answer (LINQ Execution Analysis in C#)
Code Example
using Microsoft.EntityFrameworkCore;
using System.Linq;
public class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
}
public class Program
{
public static void Main()
{
using var context = new AppDbContext();
// --- Scenario 1: Using AsEnumerable() (Client-Side) ---
// Generates: SELECT * FROM Products
// The filter (Price > 100) happens in RAM AFTER downloading everything.
var list1 = context.Products
.AsEnumerable()
.Where(p => p.Price > 100)
.ToList();
// --- Scenario 2: Using IQueryable (Server-Side) ---
// Generates: SELECT * FROM Products WHERE Price > 100
// The filter is translated to SQL and executed by the DB.
var list2 = context.Products
.Where(p => p.Price > 100)
.ToList();
}
}
Deep Dive into the Difference
- Scenario 1 (
AsEnumerable): Forces the query to transition from the database provider to your local application memory (the “Client”). EF Core must satisfy the requirement of “having a collection to iterate over,” so it pulls the entire table. If you have 1,000,000 products, you just downloaded them all to find 50. - Scenario 2 (
IQueryable): Stays in the world of Expression Trees. The.Where()call isn’t executed; it’s added to the tree. When.ToList()is called, the EF Core provider “walks” the tree, sees the “Price > 100” logic, and writes theWHEREclause into the SQL string sent to the server.
Rule of Thumb: Always keep queries as IQueryable for as long as possible. Only use AsEnumerable() or ToList() when you explicitly want to stop building the query and bring the data into memory.