Skip to content

LINQ Interview Questions & Answers

15 questions Updated 2026-06-22 Share:

LINQ interview questions — deferred execution, IQueryable vs IEnumerable, Select vs SelectMany, GroupBy, and common performance pitfalls.

Read the in-depth guideHow LINQ Works in C#: Deferred Execution and IQueryable(opens in new tab)
15 of 15

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 ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel