Language Integrated Query (LINQ) is a set of C# language features and BCL extension methods that let you query any data source — collections, XML, databases, JSON — using a uniform, type-safe syntax directly in C# code.
int[] numbers = { 5, 3, 8, 1, 9, 2 };
// Without LINQ: imperative loop
var evens = new List<int>();
foreach (var n in numbers)
if (n % 2 == 0) evens.Add(n);
// With LINQ method syntax:
var evensLinq = numbers.Where(n => n % 2 == 0)
.OrderBy(n => n)
.ToList(); // [2, 8]
// Query syntax (translates to the same method calls):
var evensQuery = (from n in numbers
where n % 2 == 0
orderby n
select n).ToList();
LINQ is not limited to in-memory collections. The same syntax works for SQL
databases via Entity Framework (IQueryable<T>), XML via LINQ to XML, and any
custom data source that implements IEnumerable<T> or IQueryable<T>.
Rule of thumb: LINQ makes collection operations declarative and composable —
you describe what you want, not how to iterate. Prefer it over imperative
loops for readability; fall back to foreach only when performance profiling
justifies it.
Deferred execution means a LINQ query is not evaluated when it is defined — it is evaluated only when you iterate the result. Each iteration re-runs the query against the current state of the source.
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// This builds a query object — no iteration yet:
var query = numbers.Where(n => n > 2); // deferred
numbers.Add(10); // modify source after query is defined
foreach (var n in query) Console.Write(n + " "); // 3 4 5 10 — sees the new item!
// Force immediate execution with ToList() / ToArray() / Count():
var snapshot = numbers.Where(n => n > 2).ToList(); // evaluated NOW
numbers.Add(99);
// 'snapshot' still contains { 3, 4, 5, 10 } — not affected by later changes
Deferred execution enables query composition — you can build a query
incrementally and it only hits the data source once when iterated. The danger is
multiple enumeration: iterating a deferred query twice runs it twice. Pass
a IEnumerable<T> to a method that iterates it twice and you pay double the cost.
Rule of thumb: Call .ToList() or .ToArray() to materialise when you will
iterate multiple times, need a point-in-time snapshot, or want to avoid re-querying
a database in EF Core.
Both syntaxes produce identical IL — the compiler translates query syntax into method calls. Query syntax is SQL-like; method syntax (fluent) uses extension methods chained with lambdas.
var data = new[] { 1, 2, 3, 4, 5, 6 };
// Query syntax:
var q1 = from n in data
where n % 2 == 0
orderby n descending
select n * n;
// Method (fluent) syntax — identical result:
var q2 = data.Where(n => n % 2 == 0)
.OrderByDescending(n => n)
.Select(n => n * n);
// Some operators only exist in method syntax (no query-syntax equivalent):
var q3 = data.Zip(data.Reverse(), (a, b) => a + b); // Zip has no query syntax
var q4 = data.Aggregate((acc, n) => acc + n); // Aggregate has no query syntax
var count = data.Count(n => n > 3); // Count with predicate — method only
Method syntax is more common in practice because it composes naturally with method chaining and covers more operators. Query syntax reads more naturally for complex multi-source joins, let clauses, and group-by queries.
Rule of thumb: Default to method syntax — it covers every operator and is
what most .NET codebases use. Switch to query syntax for complex joins or when
the let keyword (intermediate variable) would significantly aid readability.
IEnumerable<T> executes LINQ in memory (LINQ to Objects). IQueryable<T>
translates the LINQ expression tree into the query language of the data source
(typically SQL) and executes it there.
// IEnumerable: filtering happens in C# after loading ALL rows from DB
IEnumerable<User> usersEnum = dbContext.Users.ToList(); // loads ALL users first
var active = usersEnum.Where(u => u.IsActive); // filters in memory — bad!
// IQueryable: filtering is translated to SQL — only matching rows are fetched
IQueryable<User> usersQuery = dbContext.Users; // no DB hit yet
var activeQ = usersQuery.Where(u => u.IsActive); // adds WHERE to SQL expression
var result = activeQ.ToList(); // ONE SQL: SELECT * FROM Users WHERE IsActive = 1
IQueryable<T> stores an expression tree — a data structure representing the
query. When you call ToList(), the LINQ provider (EF Core, LINQ to SQL) walks
the expression tree and generates the appropriate query. IEnumerable<T> just
holds a delegate — it has no visibility into what the lambda does and cannot
translate it.
Rule of thumb: Use IQueryable<T> for database queries so filtering, sorting,
and pagination happen in SQL. Convert to IEnumerable<T> (.AsEnumerable()) only
when you need to run logic the provider cannot translate.
Select projects each element to one output element (1-to-1). SelectMany
projects each element to a sequence and flattens all those sequences into a
single output sequence (1-to-many → flat).
var orders = new[]
{
new { Id = 1, Items = new[] { "pen", "book" } },
new { Id = 2, Items = new[] { "desk", "chair", "lamp" } },
};
// Select — returns IEnumerable<string[]>: each order maps to its array
var nested = orders.Select(o => o.Items);
// Result: [ ["pen","book"], ["desk","chair","lamp"] ]
// SelectMany — flattens into IEnumerable<string>
var flat = orders.SelectMany(o => o.Items);
// Result: ["pen", "book", "desk", "chair", "lamp"]
// Common use: get all characters in all words:
var words = new[] { "hello", "world" };
var chars = words.SelectMany(w => w.ToCharArray()).Distinct().OrderBy(c => c);
// d e h l o r w
SelectMany is equivalent to a nested foreach that adds each inner element to a
flat output list. In query syntax, it corresponds to multiple from clauses.
Rule of thumb: If each source element maps to a collection and you want a
flat result, use SelectMany. If you want a sequence of sequences (or one-to-one),
use Select.
These four methods differ on how many elements are expected and what happens when the expectation is violated.
var empty = Array.Empty<int>();
var one = new[] { 42 };
var multi = new[] { 1, 2, 3 };
// First() — expects at least one; throws if empty
multi.First(); // 1
multi.First(n => n > 1); // 2
// empty.First(); // InvalidOperationException!
// FirstOrDefault() — returns default(T) if empty, no throw
empty.FirstOrDefault(); // 0 (default int)
empty.FirstOrDefault(-1); // -1 (C# 10 default value param)
// Single() — expects exactly one; throws if empty OR if more than one
one.Single(); // 42
// multi.Single(); // InvalidOperationException — more than one element!
// empty.Single(); // InvalidOperationException — sequence is empty!
// SingleOrDefault() — returns default if empty; throws if more than one
one.SingleOrDefault(); // 42
empty.SingleOrDefault(); // 0 — fine
// multi.SingleOrDefault(); // InvalidOperationException — still throws for multiple!
Rule of thumb: Use First/FirstOrDefault when you expect a list and want the
first match. Use Single/SingleOrDefault when exactly one result is a business
requirement (e.g., look up by unique ID) — the exception is a useful assertion.
GroupBy partitions elements into groups based on a key. It returns
IEnumerable<IGrouping<TKey, TElement>> — each group has a Key property and
is itself an IEnumerable<TElement>.
var products = new[]
{
new { Name = "Pen", Category = "Stationery", Price = 1.5 },
new { Name = "Book", Category = "Education", Price = 12.0 },
new { Name = "Ruler", Category = "Stationery", Price = 0.8 },
new { Name = "Calc", Category = "Education", Price = 25.0 },
};
var byCategory = products.GroupBy(p => p.Category);
foreach (var group in byCategory)
{
Console.WriteLine($"{group.Key}: {group.Count()} items, " +
$"avg £{group.Average(p => p.Price):F2}");
}
// Stationery: 2 items, avg £1.15
// Education: 2 items, avg £18.50
// With element projection:
var namesByCategory = products
.GroupBy(p => p.Category, p => p.Name)
.ToDictionary(g => g.Key, g => g.ToList());
// { "Stationery": ["Pen","Ruler"], "Education": ["Book","Calc"] }
In EF Core, GroupBy on IQueryable<T> translates to SQL GROUP BY. However,
not all LINQ GroupBy projections can be translated — EF Core will throw at runtime
if it cannot. Use .AsEnumerable() before GroupBy to force in-memory grouping
when needed.
Rule of thumb: GroupBy is for aggregating by a category. Always follow it
with Count(), Sum(), Average(), Max(), or a materialising call — iterating
an IGrouping multiple times re-evaluates the source.
These three check membership or quantity. Knowing which to use avoids common performance mistakes.
var nums = new[] { 1, 2, 3, 4, 5 };
// Any() — "does at least one element satisfy the condition?"
nums.Any(); // true (sequence is non-empty)
nums.Any(n => n > 10); // false
// All() — "do ALL elements satisfy the condition?"
nums.All(n => n > 0); // true
nums.All(n => n > 3); // false
// Count() — how many satisfy the condition?
nums.Count(); // 5
nums.Count(n => n > 3); // 2 (4 and 5)
// COMMON MISTAKE — avoid:
if (list.Count() > 0) { } // iterates entire list to count
// Prefer:
if (list.Any()) { } // stops at first element — O(1) for List<T>
For ICollection<T> (like List<T>), .Count (property, not method) is O(1).
The Count() extension method iterates when the source is not an ICollection<T>.
In EF Core, both translate to SELECT COUNT(*) in SQL, so the difference is moot
there — but Any() translates to EXISTS (SELECT 1 ...) which can be faster.
Rule of thumb: Use Any() to check emptiness or existence. Use Count() only
when you need the actual number. Never call Count() > 0 — use Any() instead.
Aggregate() is LINQ's fold (also called reduce). It accumulates a sequence
into a single value by repeatedly applying a function to a running accumulator and
the next element.
var nums = new[] { 1, 2, 3, 4, 5 };
// Sum via Aggregate (seed defaults to first element):
int sum = nums.Aggregate((acc, n) => acc + n); // 15
// With explicit seed:
int product = nums.Aggregate(1, (acc, n) => acc * n); // 120
// With result selector (seed, fold, then transform):
string csv = nums.Aggregate(
new System.Text.StringBuilder(),
(sb, n) => { sb.Append(n).Append(','); return sb; },
sb => sb.ToString().TrimEnd(',')
); // "1,2,3,4,5"
// String joining (Aggregate style vs string.Join):
string joined = new[] { "a", "b", "c" }.Aggregate((a, b) => $"{a},{b}"); // "a,b,c"
// Prefer: string.Join(",", new[] { "a","b","c" }) — more readable + efficient
Aggregate is the most general LINQ operator — Sum, Max, Min, Count, and
Average are all special cases of Aggregate. Use the specialised versions when
they exist; reach for Aggregate only when none of the purpose-built operators fit.
Rule of thumb: Aggregate = fold/reduce. Use named operators (Sum, Max)
when they exist. Use Aggregate for custom fold logic like building a running
product, combining strings with separators, or computing complex rolling statistics.
ToList() and ToArray() both materialise a LINQ query into a concrete
in-memory collection, forcing immediate execution. AsEnumerable() keeps the
query deferred but switches the compile-time type from IQueryable<T> to
IEnumerable<T>, causing subsequent operators to run in memory.
IQueryable<User> query = dbContext.Users.Where(u => u.IsActive);
// ToList() — executes SQL NOW, returns List<User>
List<User> list = query.ToList();
// ToArray() — executes SQL NOW, returns User[]
User[] array = query.ToArray();
// AsEnumerable() — no execution yet; shifts to LINQ-to-Objects for subsequent ops
IEnumerable<User> enumerable = query.AsEnumerable();
// The next Where runs in C# memory, not SQL:
var localFilter = enumerable.Where(u => MyComplexCSharpMethod(u));
// When you iterate 'localFilter', it fetches ALL active users from DB first, then filters
Use AsEnumerable() when a LINQ provider (EF Core) cannot translate a particular
operator or lambda to SQL — it forces the remainder of the pipeline to run in
memory on the already-fetched rows.
Rule of thumb: ToList() or ToArray() = execute now + store. AsEnumerable()
= switch to in-memory LINQ without executing yet (the DB query still fires when you
iterate). Choose ToList() for most cases; ToArray() when you need a fixed-size buffer.
Join produces a flat sequence where each left element is matched with each right
element on a key — equivalent to SQL INNER JOIN. GroupJoin produces a
hierarchical result — each left element paired with all its matching right elements
— equivalent to SQL LEFT OUTER JOIN (when combined with DefaultIfEmpty).
var customers = new[] {
new { Id = 1, Name = "Alice" },
new { Id = 2, Name = "Bob" },
};
var orders = new[] {
new { CustomerId = 1, Item = "Book" },
new { CustomerId = 1, Item = "Pen" },
new { CustomerId = 2, Item = "Desk" },
};
// Join — flat INNER JOIN:
var inner = customers.Join(orders,
c => c.Id, // outer key
o => o.CustomerId, // inner key
(c, o) => $"{c.Name}: {o.Item}");
// Alice: Book, Alice: Pen, Bob: Desk
// GroupJoin — hierarchical LEFT JOIN:
var grouped = customers.GroupJoin(orders,
c => c.Id,
o => o.CustomerId,
(c, orderGroup) => new { c.Name, Orders = orderGroup.ToList() });
// { Name="Alice", Orders=[Book,Pen] }, { Name="Bob", Orders=[Desk] }
In EF Core on IQueryable, Join translates to SQL INNER JOIN and GroupJoin
translates to LEFT JOIN (when .SelectMany(..., DefaultIfEmpty()) is applied).
Rule of thumb: Use Join for inner joins (only matching records). Use
GroupJoin when you need a parent with a collection of children, or when you
want to preserve left-side records even if there are no matches.
Zip merges two (or three, since .NET 6) sequences element-by-element, producing
one output element per pair. It stops when the shorter sequence is exhausted.
var names = new[] { "Alice", "Bob", "Charlie" };
var scores = new[] { 92, 85, 78 };
// Two-sequence Zip with result selector:
var results = names.Zip(scores, (name, score) => $"{name}: {score}");
// ["Alice: 92", "Bob: 85", "Charlie: 78"]
// C# 6+: Zip returning tuples (no selector needed):
var pairs = names.Zip(scores); // IEnumerable<(string, int)>
foreach (var (name, score) in pairs)
Console.WriteLine($"{name} scored {score}");
// .NET 6+ — three-way Zip:
var grades = new[] { 'A', 'B', 'C' };
var triples = names.Zip(scores, grades); // IEnumerable<(string, int, char)>
// Stops at the shorter sequence:
new[] { 1, 2, 3 }.Zip(new[] { 'a', 'b' }); // only two pairs: (1,'a'), (2,'b')
Common use cases: combining a list of keys with a list of values, pairing
chronological data from two sources, or producing numbered output by zipping with
Enumerable.Range.
Rule of thumb: Use Zip when two sequences are positionally aligned and you
want to process them in lock-step. If the sequences can have different orderings,
a Join on a key is safer.
Distinct() removes duplicate elements using equality comparison on the element
itself. DistinctBy() (.NET 6+) removes duplicates based on a key selector —
keeping the first element with each distinct key value.
var nums = new[] { 1, 2, 2, 3, 3, 3 };
var unique = nums.Distinct(); // { 1, 2, 3 }
// Custom equality for objects — requires IEqualityComparer<T>:
var people = new[]
{
new { Name = "Alice", Dept = "Eng" },
new { Name = "Bob", Dept = "HR" },
new { Name = "Alice", Dept = "HR" }, // same Name, different Dept
};
// Distinct() on anonymous types uses value equality (all fields):
// Result: all three — Name+Dept combination is different for each
// DistinctBy() — deduplicate by a single key, keeping first occurrence:
var byName = people.DistinctBy(p => p.Name);
// { Alice/Eng, Bob/HR } — second Alice dropped
foreach (var p in byName)
Console.WriteLine($"{p.Name} / {p.Dept}");
// Useful for deduplicating DB results by a business key:
var latestOrders = orders.DistinctBy(o => o.CustomerId);
Distinct() uses the default equality comparer (EqualityComparer<T>.Default).
For custom comparison pass an IEqualityComparer<T> as the second argument.
DistinctBy is the LINQ equivalent of SQL DISTINCT ON (PostgreSQL) or a
GROUP BY key HAVING ROW_NUMBER() = 1 pattern.
Rule of thumb: Use Distinct() to deduplicate primitives and value-equal types.
Use DistinctBy(x => x.Key) to pick one representative element per key from a
collection of objects.
OrderBy / OrderByDescending establishes the primary sort key. ThenBy /
ThenByDescending adds a secondary (tie-breaking) sort applied only when the
primary key values are equal.
var employees = new[]
{
new { Name = "Charlie", Dept = "Eng", Salary = 80_000 },
new { Name = "Alice", Dept = "HR", Salary = 60_000 },
new { Name = "Bob", Dept = "Eng", Salary = 90_000 },
new { Name = "Diana", Dept = "HR", Salary = 70_000 },
};
// Sort by Dept, then by Name within each dept:
var sorted = employees
.OrderBy(e => e.Dept)
.ThenBy(e => e.Name);
// Eng/Bob, Eng/Charlie, HR/Alice, HR/Diana
// Multi-level descending:
var byDeptThenSalaryDesc = employees
.OrderBy(e => e.Dept)
.ThenByDescending(e => e.Salary);
// Eng/Bob(90k), Eng/Charlie(80k), HR/Diana(70k), HR/Alice(60k)
// Note: chaining OrderBy twice is WRONG — second OrderBy replaces the first:
// Bad: employees.OrderBy(e => e.Dept).OrderBy(e => e.Name) — only sorts by Name
// Good: .OrderBy(e => e.Dept).ThenBy(e => e.Name)
LINQ's OrderBy uses a stable sort — elements with equal keys preserve their
original relative order. This makes ThenBy reliable and predictable.
Rule of thumb: Always use ThenBy / ThenByDescending for secondary sort
criteria, never chain a second OrderBy. A chained OrderBy silently discards the
previous ordering.
LINQ's composability can hide significant performance costs. The most common pitfalls
are multiple enumeration, N+1 queries in EF Core, and misuse of Count() vs Any().
// Pitfall 1 — multiple enumeration: iterates the source twice
IEnumerable<int> query = GetExpensiveSequence();
if (query.Any()) // first enumeration
Process(query.First()); // second enumeration
// Fix: materialise once
var list = query.ToList();
if (list.Count > 0) Process(list[0]);
// Pitfall 2 — N+1 query: one DB hit per order
foreach (var order in dbContext.Orders.ToList()) // 1 query
Console.WriteLine(order.Customer.Name); // N queries (lazy load)
// Fix: eager load with Include
foreach (var order in dbContext.Orders.Include(o => o.Customer).ToList())
Console.WriteLine(order.Customer.Name); // 1 query with JOIN
// Pitfall 3 — Count() on IEnumerable iterates the whole sequence
if (GetItems().Count() > 0) { } // O(n)
// Fix:
if (GetItems().Any()) { } // O(1) stops at first element
// Pitfall 4 — Where before Select on large datasets (in memory):
var result = items.Select(Transform).Where(x => x.IsValid);
// Bad: Transform() called on ALL items
// Good: filter first, then project
var result2 = items.Where(x => x.IsEligible).Select(Transform);
Rule of thumb: Materialise with ToList() when you iterate more than once.
Use Include() in EF Core to avoid N+1. Use Any() for existence checks. Filter
with Where() before projecting with Select() to minimise work.
More Fundamentals interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.