LINQ Basics
LINQ (Language Integrated Query) is a set of extension methods and language keywords that let you query any object implementing IEnumerable<T> (or IQueryable<T> for remote sources like databases) using a consistent, declarative syntax. Instead of writing manual loops with mutable accumulator variables, you express what you want — filter, project, sort, group — and the runtime handles how. LINQ ships with two equivalent syntaxes: method syntax, built from chained extension methods like Where, Select, and OrderBy defined in System.Linq, and query syntax, which reads like SQL (from x in source where ... select ...) and compiles down to the very same method calls. Most professional C# code favors method syntax because it composes better with lambdas and is easier to extend with custom operators.
Cricket analogy: LINQ is like telling the scorer 'give me all centuries this series' instead of manually flipping through every scorecard; method syntax chaining Where/Select is like a fielding captain's checklist, while query syntax reads like a commentator's script but compiles to the same instructions.
Deferred Execution and Streaming
A defining trait of most LINQ operators is deferred (lazy) execution: calling Where or Select does not iterate the source immediately — it builds an iterator that only runs when you actually enumerate the result, for example with foreach, ToList(), or Count(). This means a query variable can be built once and re-evaluated later against a source that has since changed, which is powerful but also a common source of subtle bugs if you assume a query has 'already run'. Operators like ToList, ToArray, Count, and First force immediate evaluation.
Cricket analogy: A Where filter on 'all sixes hit' is like writing the query on the scoreboard but not tallying until the umpire calls for the count at innings end (ToList/Count) - and if the match continues, re-checking the tally reflects the latest overs, not the original snapshot.
var employees = new List<Employee>
{
new("Ravi", "Engineering", 92000),
new("Meera", "Sales", 61000),
new("Owen", "Engineering", 105000),
new("Priya", "Sales", 74000),
};
// Method syntax: filter, project, sort
var topEngineers = employees
.Where(e => e.Department == "Engineering")
.OrderByDescending(e => e.Salary)
.Select(e => new { e.Name, e.Salary })
.ToList();
// Equivalent query syntax
var sameResult =
from e in employees
where e.Department == "Engineering"
orderby e.Salary descending
select new { e.Name, e.Salary };
// Grouping and aggregation
var averageByDept = employees
.GroupBy(e => e.Department)
.Select(g => new { Department = g.Key, Avg = g.Average(e => e.Salary) });
record Employee(string Name, string Department, decimal Salary);Common Operators
Beyond Where and Select, LINQ provides a rich vocabulary: Any/All for existence checks, First/FirstOrDefault/Single/SingleOrDefault for extracting one element (each with different failure semantics), Take/Skip for paging, Distinct for de-duplication, Join and GroupJoin for relational-style combining of two sequences, and Aggregate for custom reductions. Choosing First vs Single matters: First simply returns the first match (or throws/returns default if none), while Single asserts exactly one match exists and throws if there are zero or more than one — a useful invariant check when 'more than one' would indicate a data bug.
Cricket analogy: Any checks 'did anyone score a century' while All checks 'did everyone reach fifty'; First just grabs the top scorer even with ties, but Single insists there's exactly one century-maker and throws if two batsmen both hit one - useful when only one award should exist.
LINQ to Objects (over IEnumerable<T>) executes entirely in memory using compiled delegates. LINQ to Entities/SQL (over IQueryable<T>), used by EF Core, instead builds an expression tree that is translated into SQL and executed on the database — the same Where/Select syntax produces very different execution depending on which interface the source implements.
Calling a LINQ query multiple times (e.g., in a loop, or passing it to both Count() and foreach) re-executes the whole pipeline each time because of deferred execution. Against a database or slow source this can mean duplicate queries or duplicate side effects; materialize with ToList()/ToArray() once you need to reuse results.
- LINQ provides a unified, declarative way to query in-memory collections (IEnumerable<T>) and remote data sources (IQueryable<T>) with the same operator vocabulary.
- Method syntax and query syntax are interchangeable; method syntax compiles the same way and is generally preferred for composability.
- Most LINQ operators use deferred execution — the query only runs when enumerated, not when declared.
- First/FirstOrDefault tolerate multiple or zero matches differently than Single/SingleOrDefault, which enforce exactly-one semantics.
- GroupBy, Join, and Aggregate cover relational-style grouping, combining, and custom reductions without manual loops.
- IQueryable<T> sources like EF Core translate LINQ expression trees into SQL, so not all C# code inside a query is guaranteed to be translatable.
Practice what you learned
1. What does 'deferred execution' mean for most LINQ operators?
2. What is the key behavioral difference between First() and Single()?
3. Which interface does EF Core use to translate LINQ queries into SQL?
4. Which method forces immediate execution of a LINQ query?
5. What is a practical risk of re-enumerating the same deferred LINQ query multiple times against a database-backed source?
Was this page helpful?
You May Also Like
Lambda Expressions
Lambda expressions provide concise, inline syntax for anonymous functions, forming the backbone of LINQ, delegates, and functional-style C# code.
Generics Explained
Understand how C# generics let you write type-safe, reusable code that works across many types without sacrificing performance or requiring casts.
Collection Interfaces
Survey the layered hierarchy of IEnumerable, ICollection, IList, and their read-only and generic variants that unify how .NET collections are consumed.
Extension Methods
Extension methods let you add new callable methods to existing types, including sealed or third-party ones, without modifying their source or subclassing.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics