What is the difference between IEnumerable and IQueryable?

C# LINQ Intermediate 2–3 Yrs3–5 Yrs Conceptual 2 min read

Quick Answer

IEnumerable<T> represents an in-memory sequence; LINQ operators run as compiled delegates and iterate objects client-side. IQueryable<T> represents a query against a remote provider (e.g. a database); LINQ operators build an expression tree that the provider translates to SQL, so filtering and paging happen at the source. Using IEnumerable too early pulls the whole table into memory before filtering.

IEnumerable is an in-memory sequence — the LINQ operators are compiled delegates that loop over objects you already have. IQueryable represents a query that hasn't run yet against something like a database; the operators build an expression tree, and the provider, EF Core for example, turns that into SQL. So with IQueryable, a Where and a Take actually become a WHERE and a TOP in the query and only the rows you want come back. The mistake I watch for is switching to IEnumerable too early — calling AsEnumerable or ToList before the filtering — because then the whole table is loaded and the filter runs in memory. The rule is: stay IQueryable until every database-side operation is in the query, then materialise.

Both are lazily evaluated, but the crucial difference is where the work happens:

  • IEnumerable<T> — LINQ to Objects. Operators take Func<...> delegates. Where(x => x.IsActive) runs the lambda for each item already in memory.
  • IQueryable<T> — LINQ to a provider (EF Core, etc.). Operators take Expression<Func<...>>. The provider inspects the expression tree and generates a query. Execution is deferred until you enumerate (ToList, foreach, First).

The bug this question is really about:

// GOOD: WHERE + TOP run in SQL
db.Orders.Where(o => o.Total > 1000).Take(20).ToList();

// BAD: AsEnumerable() switches to LINQ to Objects,
// so ALL orders are loaded, THEN filtered in memory
db.Orders.AsEnumerable().Where(o => o.Total > 1000).Take(20).ToList();

Keep the query as IQueryable until every server-side operation is expressed, then materialise. Watch for calling a C# method inside the predicate that EF can't translate — it will throw or silently evaluate client-side depending on version/config.

C# — where the filter runs
// IQueryable: translated to  SELECT TOP 20 ... WHERE Total > 1000
List<Order> a = _db.Orders
    .Where(o => o.Total > 1000)
    .OrderByDescending(o => o.OrderDate)
    .Take(20)
    .ToList();

// Forcing client evaluation: loads every row first
List<Order> b = _db.Orders
    .AsEnumerable()               // now IEnumerable -> LINQ to Objects
    .Where(o => o.Total > 1000)
    .Take(20)
    .ToList();