The last post ended with a lambda handed to FindAll. That was LINQ with the badge off. This post puts the badge on. LINQ is a set of methods that take lambdas and do the loop for you, and once it clicks you stop writing foreach for anything that filters, sorts, or reshapes a collection. Same Book class as the rest of the series.
The problem LINQ solves
Here is the data we have been carrying through the series:
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public int Price { get; set; }
}
Now a normal request: give me the titles of every book over 10, cheapest first. Without LINQ:
var expensive = new List<Book>();
foreach (var book in books)
{
if (book.Price > 10)
expensive.Add(book);
}
expensive.Sort((a, b) => a.Price.CompareTo(b.Price));
var titles = new List<string>();
foreach (var book in expensive)
{
titles.Add(book.Title);
}
Twelve lines, two temporary lists, three loops’ worth of mechanics. And notice how little of it is the actual question. The question was “over 10, cheapest first, titles only”. Everything else is plumbing: declaring accumulators, adding to them, iterating them again.
The LINQ version
var titles = books
.Where(b => b.Price > 10)
.OrderBy(b => b.Price)
.Select(b => b.Title);
Three lines, and each one is a word from the original question. No accumulators, no temporary lists, no foreach. The lambdas are exactly the ones from the previous post, and the methods are the loops you no longer write.
Read the three verbs, because most of LINQ is these three:
Wherekeeps the items where the lambda returns true. Filter. Count in, fewer out.OrderBysorts by whatever the lambda returns. Same items, new order.Selectturns each item into something else. Same count, different shape. Here,Bookin,stringout.
Select is the one beginners misread, because in SQL SELECT means “fetch”. In LINQ it means “transform”. Select(b => b.Title) does not fetch titles from anywhere, it maps a sequence of books into a sequence of strings.
Where the methods come from
Where, OrderBy and Select are not methods on List<T>. Open the source of List<T> and you will not find them. They are extension methods defined in System.Linq on IEnumerable<T>, which is why you need using System.Linq; for them to appear, and why the same methods work on arrays, List<T>, Dictionary<K,V>, and anything else you can foreach over. That mechanism is the next post in the series; for now the practical rule is: if IntelliSense does not offer Where, the using is missing.
The operators worth memorising
There are about fifty LINQ methods. You will use roughly twelve.
books.Where(b => b.Price > 10); // filter
books.Select(b => b.Title); // transform
books.OrderBy(b => b.Price); // sort ascending
books.OrderByDescending(b => b.Price); // sort descending
books.ThenBy(b => b.Title); // secondary sort, after an OrderBy
books.First(b => b.Price > 10); // first match, throws if none
books.FirstOrDefault(b => b.Price > 99); // first match, null if none
books.Single(b => b.Id == 3); // exactly one match, throws if 0 or 2+
books.Any(b => b.Price > 14); // true if at least one matches
books.All(b => b.Price > 5); // true if every one matches
books.Count(b => b.Price > 10); // how many match
books.Sum(b => b.Price); // 56
books.Average(b => b.Price); // 11.2
books.Max(b => b.Price); // 15
books.Skip(2).Take(2); // paging: skip 2, take the next 2
books.Distinct(); // remove duplicates
books.GroupBy(b => b.Author); // one group per author
Two of these are worth pausing on, because they cause real bugs.
First vs FirstOrDefault. First throws InvalidOperationException when nothing matches. FirstOrDefault returns null for a class, or 0 for an int. Use First when a missing match is a bug you want to hear about immediately, and FirstOrDefault when “none” is a legitimate answer you are going to handle. Reaching for FirstOrDefault everywhere does not remove the crash, it just moves it to a NullReferenceException later, further from the cause.
Single means “there must be exactly one”. It throws if there are zero and it throws if there are two. That sounds annoying until you use it for primary key lookups, where two matches means your data is broken and you want to know now rather than in a report next month.
The other syntax
Everything above is method syntax. C# also has query syntax, which looks like SQL with the clauses reordered:
var titles = from b in books
where b.Price > 10
orderby b.Price
select b.Title;
That compiles into exactly the method-syntax version. It is not a separate feature, it is sugar; the compiler rewrites it into .Where().OrderBy().Select() before anything runs. Query syntax reads nicely for joins and group by, and cannot express operators like Count() or Any() without dropping back into method calls, which is why most C# codebases standardise on method syntax. Know both, because interviewers ask, and pick one per codebase.
Deferred execution, the part that surprises people
This is the most important LINQ behaviour and the most common interview question on it.
var query = books.Where(b => b.Price > 10); // nothing has run yet
books.Add(new Book { Id = 6, Title = "Dune", Price = 20 });
foreach (var b in query) // NOW it runs
Console.WriteLine(b.Title); // Dune is included
Where did not filter anything on the line where you called it. It built a query object that knows how to filter, and that object sat there doing nothing. The work happens when something iterates the result: a foreach, or one of the methods that has to produce a final answer, like ToList(), ToArray(), Count(), First() or Sum().
Two consequences follow, and both show up in production code.
The query re-runs every time you iterate it. Loop over the same IEnumerable query three times and you filtered three times. If the source is a database, that is three round trips. Call ToList() once and reuse the list.
The query sees the source as it is at execution time, not at declaration time. That is why Dune appeared above. Useful when you want it, baffling when you do not.
The rule of thumb: Where, Select, OrderBy, Skip, Take and GroupBy are lazy. ToList, ToArray, Count, First, Any and Sum are eager and force execution. When you want a snapshot, call ToList().
IEnumerable vs IQueryable
The same LINQ code can mean two completely different things, and this is where LINQ stops being a convenience and becomes architecture.
IEnumerable<Book> a = dbContext.Books; // careful
IQueryable<Book> b = dbContext.Books; // normal
IEnumerable<T>is LINQ to Objects. The lambdas are compiled code. Filtering happens in memory, in your process.IQueryable<T>is LINQ to a provider such as EF Core. The lambdas are captured as expression trees, the data structure from the end of the lambdas post, and the provider translates them into SQL.
So this:
var cheap = dbContext.Books.Where(b => b.Price < 10).ToList();
becomes SELECT ... FROM Books WHERE Price < 10, and only the matching rows cross the wire. But cast to IEnumerable first, or call .ToList() too early, and EF Core loads the entire table into memory and filters it there:
var cheap = dbContext.Books.ToList().Where(b => b.Price < 10); // loads every book, then filters
Those two lines look nearly identical in a code review and differ by a full table scan in production. The rule: keep the query as IQueryable, and put ToList() at the very end, after every Where.
The same idea inside Microsoft, Google and Netflix
Once you know the shape you see it in every ecosystem, and it always arrived for the same reason: people were tired of writing accumulator loops.
Java (Netflix, most of the JVM world). Streams are LINQ with different names, added in Java 8 for exactly this:
List<String> titles = books.stream()
.filter(b -> b.getPrice() > 10)
.sorted(Comparator.comparing(Book::getPrice))
.map(Book::getTitle)
.toList();
filter is Where, map is Select, sorted is OrderBy, and toList() is the terminal operation that forces execution. Java streams are lazy in the same way, for the same reasons.
JavaScript (Meta, every frontend). Arrays got the same three verbs, though they are eager rather than deferred:
const titles = books.filter(b => b.price > 10).map(b => b.title);
React renders lists with .map() millions of times a second worldwide. That is Select.
Kotlin (Google, Android). Almost character for character:
val titles = books.filter { it.price > 10 }.sortedBy { it.price }.map { it.title }
Python. A comprehension is the same query written as syntax rather than methods:
titles = [b.title for b in sorted(books, key=lambda b: b.price) if b.price > 10]
Different names for the same three operations: keep some, reorder, reshape. Learn them once in C# and you can read data code in any of these languages on day one.
Summary
- LINQ is a set of extension methods on
IEnumerable<T>that take lambdas and do the looping for you. - The three you use constantly:
Wherefilters,OrderBysorts,Selecttransforms.Selectmeans map, not fetch. Firstthrows when nothing matches,FirstOrDefaultreturns null,Singledemands exactly one.- Query syntax (
from … where … select) compiles to the same method calls. It is sugar, not a separate feature. - Most operators are deferred: they build a query and run it when you iterate.
ToList(),Count()andFirst()force execution. - A deferred query re-runs on every iteration and reads the source at execution time, not declaration time.
IEnumerable<T>filters in memory;IQueryable<T>translates your lambdas into SQL. PutToList()last, not first.
Interview questions and answers
1. What is LINQ?
Language Integrated Query: a set of extension methods in System.Linq on IEnumerable<T> and IQueryable<T> that let you filter, sort, project, group and aggregate collections using lambdas, so the code reads like the question you are asking rather than the loop that answers it.
2. What is the difference between Where and Select?
Where filters: it takes a Func<T, bool> and returns the items that matched, so the element type stays the same and the count can shrink. Select projects: it takes a Func<T, TResult> and transforms every item, so the count stays the same and the element type can change.
3. What is deferred execution?
Most LINQ operators do not run when you call them. They build a query object, and the work happens the moment something iterates the result: a foreach, or a terminal operator like ToList, Count, First or Sum. It avoids doing work you never consume and lets a whole chain of operators run in a single pass.
4. What are the practical consequences of deferred execution?
Two. The query re-executes every time you enumerate it, so iterating a database query three times means three round trips. And the query reads the source at execution time, so changes made to the collection after the query was declared appear in the results. Call ToList() when you want a stable snapshot.
5. Which LINQ methods force immediate execution?
The ones that must produce a concrete answer: ToList, ToArray, ToDictionary, ToHashSet, Count, Sum, Average, Min, Max, First, FirstOrDefault, Single, Any and All. Anything returning a single value or a materialised collection cannot be lazy.
6. What is the difference between IEnumerable and IQueryable?
IEnumerable<T> holds compiled delegates and executes in memory in your process. IQueryable<T> holds expression trees, which a provider such as EF Core inspects and translates into another language, usually SQL, so filtering happens in the database. Assigning an IQueryable to an IEnumerable variable, or calling ToList() too early, silently moves the work into memory and can pull a whole table across the wire.
7. What is the difference between First, FirstOrDefault and Single?
First returns the first match and throws if there are none. FirstOrDefault returns the first match or the type’s default (null for reference types, 0 for int). Single returns the only match and throws if there are zero or more than one, which makes it the right choice for primary key lookups where duplicates mean corrupted data.
8. What is the difference between query syntax and method syntax?
Nothing at runtime. Query syntax is compiler sugar rewritten into method calls during compilation. Query syntax reads better for joins and grouping; method syntax covers every operator, including ones query syntax has no keyword for, which is why most codebases standardise on it.
9. Why does LINQ need lambdas and delegates?
Because every LINQ operator is a method that accepts behaviour as a parameter. Where takes a Func<T, bool>, Select takes a Func<T, TResult>, OrderBy takes a key selector. LINQ is the largest practical example of the delegates idea: the library owns the loop, you supply the decision.
10. How do you avoid the N+1 problem with LINQ and EF Core?
It happens when you materialise a list of parents and then touch a navigation property inside a loop, firing one query per row. Fix it by loading the related data in the same query with Include, or by projecting exactly the fields you need with Select into a DTO, so a single SQL statement returns everything.