Polyglot
Languages C# linq
C# § linq

LINQ

Language-Integrated Query (LINQ) is the C# facility for querying collections and other data sources through a uniform expression language. The standard query operators — Where, Select, OrderBy, GroupBy, Join, SelectMany, Aggregate, ToList, ToDictionary — operate on IEnumerable<T> (in-memory collections) and on IQueryable<T> (translated to other forms, classically SQL). The mechanism is one of the largest C# innovations: it admits expressing what older C# code wrote as explicit loops in a declarative form that is more concise, more composable, and (often) more efficient.

LINQ is a coordinated combination of three things: a set of standard query operators implemented as extension methods, a query syntax that compiles to calls of those operators, and the expression-tree infrastructure that admits LINQ providers (such as Entity Framework Core) translating queries into other languages. This page covers the surface a working programmer encounters routinely; the underlying collections are treated in Data structures.

The two surface syntaxes

LINQ is implemented through extension methods on IEnumerable<T> (and on IQueryable<T> for queryable providers). Two equivalent surface syntaxes:

Method syntax

var evens = numbers.Where(n => n % 2 == 0)
                    .Select(n => n * n)
                    .ToList();

var byCategory = items.GroupBy(i => i.Category)
                       .ToDictionary(g => g.Key, g => g.Count());

The method syntax is the conventional contemporary form. Extension methods chain through .; lambda expressions specify per-element behaviour.

Query syntax

var evens = (from n in numbers
              where n % 2 == 0
              select n * n).ToList();

var byCategory = (from i in items
                   group i by i.Category into g
                   select new { Category = g.Key, Count = g.Count() }).ToList();

The query syntax is more SQL-like and is occasionally clearer for joins and groupings. The two forms compile to the same code; the choice is stylistic.

In practice, the method syntax dominates in modern C# code, except for joins and complex group-bys, where the query syntax is often clearer.

Standard query operators

The principal operators:

OperatorEffect
Where(pred)Keep elements where pred(x) is true.
Select(fn)Project each element through fn.
SelectMany(fn)Project each element to a collection and flatten.
OrderBy(key) / OrderByDescending(key)Sort by key.
ThenBy(key) / ThenByDescending(key)Secondary sort key.
GroupBy(key)Group by key. Result: IEnumerable<IGrouping<TKey, TElement>>.
Join(other, key1, key2, result)Inner join on keys.
GroupJoin(other, key1, key2, result)Left outer join.
Distinct()Remove duplicates.
DistinctBy(key)Distinct by a derived key (.NET 6+).
Union(other), Intersect(other), Except(other)Set operations.
Take(n) / Skip(n)First n / skip n.
TakeWhile(pred) / SkipWhile(pred)While predicate is true.
TakeLast(n) / SkipLast(n)Last n / skip last n.
First() / FirstOrDefault()First element (or default).
Single() / SingleOrDefault()Exactly one element.
Last() / LastOrDefault()Last element.
ElementAt(i) / ElementAtOrDefault(i)Element by index.
Count() / LongCount()Element count.
Sum(fn) / Average(fn) / Min(fn) / Max(fn)Aggregations.
MinBy(key) / MaxBy(key)Element with min/max key (.NET 6+).
Aggregate(seed, fn)General reduction.
Any() / Any(pred)Existence test.
All(pred)Universal test.
Contains(value)Membership test.
Concat(other)Concatenate sequences.
Zip(other, fn)Pair elements from two sequences.
Chunk(size)Split into chunks (.NET 6+).
Reverse()Reverse order.
OfType<T>() / Cast<T>()Filter or cast to type.
ToList() / ToArray()Materialise as List<T> / T[].
ToDictionary(key) / ToDictionary(key, value)Materialise as dictionary.
ToHashSet()Materialise as HashSet<T>.
ToLookup(key)Materialise as one-to-many lookup.
var orders = LoadOrders();

// Total per customer:
var perCustomer = orders.GroupBy(o => o.CustomerId)
                         .ToDictionary(
                             g => g.Key,
                             g => g.Sum(o => o.Amount)
                         );

// Top 5 customers by total order value:
var top5 = orders.GroupBy(o => o.CustomerId)
                  .OrderByDescending(g => g.Sum(o => o.Amount))
                  .Take(5)
                  .Select(g => new { CustomerId = g.Key, Total = g.Sum(o => o.Amount) })
                  .ToList();

// Customer with the largest single order:
var biggest = orders.MaxBy(o => o.Amount);

IEnumerable<T> and the LINQ-to-objects model

Most LINQ usage is LINQ to objects: queries operate on in-memory IEnumerable<T> (lists, arrays, dictionary values, query results). The operators are extension methods on IEnumerable<T>; they execute in the C# runtime, processing each element in turn.

Each operator is implemented as an iterator:

public static class Enumerable {
    public static IEnumerable<TResult> Select<TSource, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, TResult> selector
    ) {
        foreach (var item in source) {
            yield return selector(item);
        }
    }

    public static IEnumerable<T> Where<T>(
        this IEnumerable<T> source,
        Func<T, bool> predicate
    ) {
        foreach (var item in source) {
            if (predicate(item)) yield return item;
        }
    }
}

Each operator returns an IEnumerable<TResult> that the next operator consumes; the chain is a pipeline of generators. Items are processed lazily, one at a time, only when the result is enumerated.

IQueryable<T> and provider-based LINQ

A LINQ provider adapts the operator surface to a different execution model. The principal example is Entity Framework Core, which translates LINQ queries into SQL:

using var db = new MyDbContext();

var customers = db.Customers
                   .Where(c => c.Country == "UK")
                   .OrderBy(c => c.Name)
                   .Take(10)
                   .ToList();

// Translated to:
//   SELECT TOP 10 * FROM Customers WHERE Country = 'UK' ORDER BY Name

The provider’s IQueryable<T> accepts the same operators but translates them rather than executing them. The lambda is parsed as an expression tree — a structured representation of the lambda’s body — that the provider analyses and converts to SQL.

The mechanism unifies in-memory and external-store queries under one surface: the same Where/Select/OrderBy work against List<T> (executed locally) and DbSet<T> (executed remotely).

The trade-offs:

  • The provider must support each operator; some operators (custom methods, complex projections) may not translate.
  • The translation may be inefficient if the LINQ pattern does not map cleanly to SQL.
  • Debugging the SQL emitted is occasionally necessary.

Common LINQ providers:

ProviderTranslates to
LINQ to objectsIn-process iteration
Entity Framework CoreSQL
LINQ to XMLXPath-like XML traversal
LINQ to JSON (Newtonsoft.Json, System.Text.Json)JSON traversal
MongoDB driverMongoDB query language

The code is the same shape regardless of the provider; the underlying execution differs.

Expression trees

For LINQ-to-objects, lambdas are compiled to delegates and invoked normally. For IQueryable<T>, lambdas are compiled to expression trees — runtime data structures representing the lambda’s source:

using System.Linq.Expressions;

Expression<Func<int, int>> expr = x => x * 2;

// expr is now a tree node:
//   LambdaExpression
//     Parameters: [x]
//     Body: BinaryExpression(Multiply, ParameterExpression(x), ConstantExpression(2))

Func<int, int> compiled = expr.Compile();    // produce a delegate
int result = compiled(5);                     // 10

The tree may be inspected, traversed, and translated. EF Core’s translation operates on these trees: for each LINQ operator’s lambda, the provider walks the expression tree and emits the corresponding SQL.

For most LINQ-to-objects code, expression trees are invisible; for LINQ providers, they are the foundation.

Deferred execution

LINQ operators on IEnumerable<T> are deferred: the query is not executed when defined, only when enumerated:

var query = numbers.Where(n => n > 0).Select(n => n * 2);
// Nothing has executed yet.

var list = query.ToList();
// Now everything runs.

foreach (var x in query) { /* ... */ }
// Re-runs the entire query.

The principal consequence: enumerating the same query twice runs it twice. For expensive queries, materialise the result with ToList() or ToArray():

var materialised = query.ToList();
foreach (var x in materialised) { /* ... */ }    // walks the list once
foreach (var x in materialised) { /* ... */ }    // walks the list again, no re-computation

The deferred behaviour is the conventional choice for queries that may not need to materialise; Where(...).Take(10) is efficient because only the first ten elements are evaluated.

Materialisation

Five terminal operators force materialisation:

OperatorResult
ToList()List<T>
ToArray()T[]
ToDictionary(key, value)Dictionary<TKey, TValue>
ToHashSet()HashSet<T>
ToLookup(key)ILookup<TKey, TElement> (one-to-many)

These materialise the entire query into a concrete collection. After materialisation, the resulting collection is independent of the source.

The non-terminal aggregation operators (Sum, Count, Any, All, First, etc.) also force enumeration, but they produce a single value rather than a collection.

Common patterns

Filtering

var active = users.Where(u => u.IsActive);

Mapping

var names = users.Select(u => u.Name);
var pairs = users.Select(u => new { u.Id, u.Name });        // anonymous type
var tuples = users.Select(u => (u.Id, u.Name));              // tuple

Mapping with index

var indexed = users.Select((u, i) => new { Index = i, User = u });

The Select overload that takes Func<T, int, TResult> provides the index. Useful when the position in the source matters.

Grouping

var byCountry = users.GroupBy(u => u.Country);
foreach (var group in byCountry) {
    Console.WriteLine($"{group.Key}: {group.Count()} users");
}

Joining

var joined = orders.Join(
    customers,
    o => o.CustomerId,
    c => c.Id,
    (o, c) => new { c.Name, o.Amount }
);

The query syntax is often clearer for joins:

var joined = from o in orders
             join c in customers on o.CustomerId equals c.Id
             select new { c.Name, o.Amount };

Aggregation

int total       = orders.Sum(o => o.Amount);
double avg      = orders.Average(o => o.Amount);
decimal max     = orders.Max(o => o.Amount);

var withMax     = orders.MaxBy(o => o.Amount);    // .NET 6+

Custom reduction

string concatenated = words.Aggregate("", (acc, w) => acc + " " + w);

Aggregate is the general reduce operator. The conventional alternatives — Sum, Min, Max — handle the common cases.

Lookup

var byId   = orders.ToDictionary(o => o.Id);
var idToName = users.ToDictionary(u => u.Id, u => u.Name);

The first form requires unique keys; duplicate keys throw ArgumentException. The second is the key-and-value form.

For one-to-many keys, ToLookup:

var byCountry = users.ToLookup(u => u.Country);
foreach (var country in byCountry) {
    Console.WriteLine($"{country.Key}: {country.Count()} users");
}

Pagination

var page = items.OrderBy(i => i.Id).Skip(offset).Take(pageSize).ToList();

Existence and quantification

bool any    = orders.Any(o => o.Amount > 1000);
bool all    = users.All(u => u.IsActive);
int  count  = orders.Count(o => o.Date == DateTime.Today);

Set operations

var unique  = items.Distinct();
var union   = a.Union(b);
var common  = a.Intersect(b);
var diff    = a.Except(b);

The set operations use Equals and GetHashCode of the element type; for custom types, supply an IEqualityComparer<T>.

Pitfalls

Multiple enumeration

A query enumerated twice is computed twice:

var query = ExpensiveOperation().Where(x => x.IsValid);

int count = query.Count();      // computed
foreach (var x in query) { }    // computed again

The defence is ToList() if the query is expensive and will be enumerated multiple times.

Captured variables in deferred queries

A query holds references to its captured variables; modifying them after the query is defined affects subsequent enumerations:

int threshold = 5;
var q = numbers.Where(n => n > threshold);

threshold = 10;
foreach (var n in q) { /* uses threshold = 10 */ }

The behaviour is correct but occasionally surprising; the defence is to either materialise immediately or pass the threshold by value.

First() versus FirstOrDefault()

First() throws InvalidOperationException if the source is empty; FirstOrDefault() returns default(T) (null for reference types, 0 for numeric types). Choose explicitly:

// Throws if no match:
var item = items.First(i => i.Id == id);

// Returns null if no match:
var item = items.FirstOrDefault(i => i.Id == id);
if (item is null) { /* not found */ }

The conventional discipline: use First() when no-match is unexpected (a programming bug), FirstOrDefault() when no-match is part of the contract.

Performance of repeated Count()

Count() on IEnumerable<T> enumerates the entire source. For materialised collections, Count is O(1); for queries, Count() is O(n).

var query = LargeCollection.Where(x => x.Match);

if (query.Count() > 0) /* iterates everything */
if (query.Any())       /* iterates only until the first match */

Any() is the right operator for “is there anything?”; Count() > 0 re-iterates and is slower.

OrderBy and stability

OrderBy is stable: equal elements keep their relative order. Custom comparers must be careful: returning zero from a comparer must mean “actually equal”, not “I don’t care about the order”. Inconsistent comparers can produce unpredictable results in OrderBy.

Provider-specific limitations

For IQueryable<T> providers, not every operator translates. Calling a custom method or referencing a non-translatable expression typically throws at execution time:

// May fail at runtime if MyHelper is not translatable to SQL:
var results = db.Items.Where(i => MyHelper.Matches(i.Name)).ToList();

The defence is to materialise the queryable portion first, then apply the in-memory operations:

var queryable = db.Items.Where(i => i.IsActive).ToList();    // SQL
var inMemory  = queryable.Where(i => MyHelper.Matches(i.Name)).ToList();  // C#

The boundary between the queryable and the in-memory parts must be drawn explicitly.

A note on Span<T> and high-performance alternatives

LINQ allocates: each operator returns an enumerable that may itself be an iterator object; chained operators allocate per stage. For most application code the cost is negligible; for hot paths it is meaningful.

The high-performance alternatives:

  • Manual loops: an explicit foreach with accumulators avoids the enumerator allocations.
  • Span<T>: span-based operations on contiguous memory avoid allocation.
  • MemoryExtensions: methods like Sum, Min, Max, Count on Span<T> (and arrays).
// LINQ:
int total = arr.Where(x => x > 0).Sum();

// Manual:
int total = 0;
foreach (var x in arr) if (x > 0) total += x;

The difference is rarely worth the readability cost in application code; for library and performance-critical code, the manual form is conventional.