LINQ — Language Integrated Query
In short: LINQ (Language Integrated Query, C# 3.0/.NET 3.5) gives a uniform, strongly typed query syntax for in-memory collections, databases (EF Core), XML and more.
- Advantages: compile-time checking and IntelliSense, less boilerplate than loops, readable declarative code, composable queries, deferred execution, and one skill across data sources.
In short: A deferred query isn't executed when defined.
- It runs each time it is enumerated, so it sees the current data.
Where,Select,OrderBy,GroupBy,Skip/TakeandJoinare deferred.ToList,ToArray,ToDictionary,Count,Sum,Max,First,Single,Any,AllandAggregateexecute immediately.- Pitfalls: enumerating the same query twice runs it twice; captured variables are read at execution time, not definition time.
In short: First returns the first match and throws if there are none.
FirstOrDefaultreturns default/null if there are none.Singleexpects exactly one match and throws for none or several.SingleOrDefaultreturns default for none and throws for several.- Use
Singlewhen duplicates would indicate a data bug (unique keys). - Use
Firstwith anOrderByfor "top 1". - In EF,
Singlefetches 2 rows to check uniqueness.
In short: Select projects each element to exactly one result; if the result is a collection you get nested collections.
SelectManyprojects each element to a sequence and flattens them into one sequence.- Example:
orders.Select(o => o.Lines)givesIEnumerable<List<Line>>, whileorders.SelectMany(o => o.Lines)givesIEnumerable<Line>.
In short: IEnumerable<T> executes in memory with compiled delegates (LINQ to Objects).
IQueryable<T>builds an expression tree that a provider (EF Core) translates into SQL and executes in the database.- Filtering an
IQueryablereduces the rows transferred; casting aDbSettoIEnumerablebeforeWhereloads the whole table into memory. - Keep queries as
IQueryableuntil the final materialisation.
In short: They compile to the same method calls.
- Query syntax reads like SQL and is convenient for joins,
letand grouping. - Method syntax supports every operator (
Count,Distinct,Take,Any...) and is more common in codebases. - They can be mixed:
(from p in products where p.Stock > 0 select p).Count().
In short: Before .NET 10: GroupJoin followed by SelectMany with DefaultIfEmpty().
- In query syntax: `join o in orders on c.Id equals o.CustomerId into g from o in g.DefaultIfEmpty() select new { c.Name, Total = o == null ?
- 0 : o.Total }
. .NET 10 adds aLeftJoinoperator, and EF Core 10 translates it toLEFT JOIN`. - In EF Core you often don't need explicit joins at all: navigation properties generate them.
var second = employees.Select(e => e.Salary).Distinct()
.OrderByDescending(s => s).Skip(1).FirstOrDefault();
// Nth highest: .Skip(n - 1)
// with ties (employees having that salary):
var who = employees.Where(e => e.Salary == second);In short: GroupBy(key) followed by Select(g => new { g.Key, Count = g.Count(), Total = g.Sum(x => x.Amount) }).
- Group by multiple keys with an anonymous type:
GroupBy(o => new { o.CustomerId, o.Date.Year }). - Filter groups with
Whereafter grouping (like SQLHAVING). .NET 9 addsCountByandAggregateByfor the common cases.
Any() and Count() > 0?In short: Any() stops at the first element (O(1) for most sequences); in EF it generates EXISTS, which is efficient.
Count()enumerates everything (for non-collections) or runsCOUNT(*)over all matching rows.- Use
Any()to check existence. - (For
ICollection<T>,Count()uses theCountproperty, butAny()is still clearer.)
ToList(), ToArray(), ToDictionary() and ToLookup()?In short: All materialise immediately.
ToListgives a growableList<T>,ToArraya fixed array (slightly more compact).ToDictionary(key)maps unique keys to values and throws on duplicate keys.ToLookup(key)groups into a key → many-values structure, allows duplicates, and returns an empty sequence for missing keys.
Union and Concat? Distinct with custom objects?In short: Concat appends one sequence to another, keeping duplicates.
Unionreturns distinct elements from both.- For classes,
Distinct/Unionuse default equality (reference equality unlessEquals/GetHashCodeare overridden). - Use records, implement
IEquatable<T>, pass anIEqualityComparer<T>, or useDistinctBy/UnionBy(.NET 6).
let do in query syntax?In short: It introduces a range variable that stores an intermediate calculation, so you don't repeat it: from p in products let discounted = p.Price * 0.9m where discounted > 1000 select new { p.Name, discounted }.
- It compiles to a
Selectinto an anonymous type.
In short: An expression tree (Expression<Func<T,bool>>) represents code as an inspectable data structure instead of compiled IL.
- EF Core walks the tree to understand the lambda (
o => o.Total > 1000) and translate it into SQL. - That's why only translatable constructs work in
IQueryablequeries. - You can build expression trees dynamically for runtime filters, or compile them into delegates.
In short: One query loads N parent rows, then accessing a navigation property per row triggers N extra queries (lazy loading, or querying inside a loop).
- Fix: eager load with
Include/ThenInclude, project withSelectinto DTOs (one SQL with joins/subqueries), useAsSplitQuery()for multiple collection includes, or batch-load children withWhere(x => ids.Contains(x.ParentId)). - Detect it with EF logging, MiniProfiler or Application Insights dependency counts.
In short: The expression contains something the provider can't convert to SQL: a custom C# method, an unsupported .NET method, complex client-side logic, or certain GroupBy shapes.
- EF Core 3.0+ throws instead of silently evaluating on the client (except in the final projection).
- Fix: rewrite with translatable operations, move the logic after
AsEnumerable()/ToListAsync()(only after filtering), map a DB function (HasDbFunction) or use raw SQL.
In short: For CPU-bound operations on large in-memory collections where each element's work is independent (image processing, heavy calculations): AsParallel() spreads the work across cores.
- Not for I/O-bound work (use async with
Task.WhenAll/Parallel.ForEachAsync), small collections (overhead dominates), order-sensitive results (unlessAsOrdered, which costs), or code with shared mutable state (race conditions).
db.Orders.ToList().Where(o => o.CustomerId == id) and is slow with 2 million orders. What's wrong?In short: ToList() executes immediately and loads all 2 million rows into memory, then filters in C#.
- Move the filter before materialisation:
await db.Orders.Where(o => o.CustomerId == id).Select(o => new OrderDto(...)).ToListAsync(). - The
WHEREthen runs in SQL with an index onCustomerId. - Add paging and
AsNoTracking, and project only the needed columns.
var result = employees
.GroupBy(e => e.Department)
.Select(g => new
{
Department = g.Key,
TopEarner = g.MaxBy(e => e.Salary)!.Name,
AverageSalary = g.Average(e => e.Salary),
Headcount = g.Count()
})
.OrderByDescending(x => x.AverageSalary);For ties at the top, use g.Where(e => e.Salary == g.Max(x => x.Salary)).
var duplicates = customers.GroupBy(c => c.Email.ToLowerInvariant())
.Where(g => g.Count() > 1)
.Select(g => new { Email = g.Key, Count = g.Count() });
var deduped = customers.DistinctBy(c => c.Email.ToLowerInvariant()).ToList(); // keeps first occurrence
// pre-.NET 6: customers.GroupBy(c => c.Email).Select(g => g.First())IEnumerable<Order> from a DB query, and two foreach loops over it hit the database twice. Why, and how do you fix it?In short: The method returns a deferred query, so each enumeration executes it again (two SQL round trips, possibly different results).
- Materialise once with
ToList()/ToListAsync(), and returnIReadOnlyList<Order>to make it explicit. - Analyzers (CA1851, ReSharper's "possible multiple enumeration") warn about this.