LINQ Basics

LINQ (Language Integrated Query) is a set of methods for filtering, transforming, sorting and summarising any collection. Instead of writing loops, you describe what you want — "the names of active customers, sorted by city" — and chain methods together. The same style works on arrays, lists, dictionaries, XML, and databases through Entity Framework.

A first query

C#
int[] numbers = [5, 12, 8, 21, 3, 16, 9];

var result = numbers
    .Where(n => n > 6)
    .OrderBy(n => n)
    .Select(n => n * 10);

Console.WriteLine(string.Join(", ", result));
Output
80, 90, 120, 160, 210

Read it top to bottom: keep the numbers greater than 6, sort them, multiply each by 10. Each method takes a lambda and returns a new sequence; the original array is never modified. LINQ methods are extension methods on IEnumerable<T> in the System.Linq namespace, which is imported automatically.

Sample data

The rest of this lesson uses a small list of products:

C#
List<Product> products =
[
    new("Laptop", "Electronics", 999m, 5),
    new("Headphones", "Electronics", 149m, 0),
    new("Desk", "Furniture", 249m, 3),
    new("Chair", "Furniture", 129m, 12),
    new("Monitor", "Electronics", 299m, 7),
    new("Lamp", "Furniture", 39m, 20),
];

var inStock = products.Where(p => p.Stock > 0);
var names = inStock.Select(p => p.Name);
Console.WriteLine(string.Join(", ", names));

var cheapFurniture = products.Where(p => p.Category == "Furniture" && p.Price < 200m);
foreach (var p in cheapFurniture)
    Console.WriteLine($"{p.Name}: {p.Price:C}");

record Product(string Name, string Category, decimal Price, int Stock);
Output
Laptop, Desk, Chair, Monitor, Lamp
Chair: $129.00
Lamp: $39.00

Select: transforming

Select turns each element into something else — a property, a calculation, or a new object. Anonymous types (new { … }) are handy for quick shapes:

C#
List<Product> products = [new("Laptop", 999m, 5), new("Desk", 249m, 3), new("Lamp", 39m, 20)];

var values = products.Select(p => new { p.Name, Value = p.Price * p.Stock });
foreach (var v in values)
    Console.WriteLine($"{v.Name,-7}{v.Value,10:C}");

var numbered = products.Select((p, index) => $"{index + 1}. {p.Name}");
Console.WriteLine(string.Join("  ", numbered));

record Product(string Name, decimal Price, int Stock);
Output
Laptop  $4,995.00
Desk      $747.00
Lamp      $780.00
1. Laptop  2. Desk  3. Lamp

Sorting

OrderBy and OrderByDescending sort by a key; ThenBy adds a tie-breaker. Order() sorts simple values by their natural order:

C#
List<Product> products =
[
    new("Laptop", "Electronics", 999m), new("Headphones", "Electronics", 149m),
    new("Desk", "Furniture", 249m), new("Chair", "Furniture", 129m), new("Monitor", "Electronics", 299m),
];

foreach (var p in products.OrderBy(p => p.Category).ThenByDescending(p => p.Price))
    Console.WriteLine($"{p.Category,-12}{p.Name,-11}{p.Price,5}");

int[] scores = [72, 95, 61, 88];
Console.WriteLine(string.Join(", ", scores.Order()));
Console.WriteLine(string.Join(", ", scores.OrderDescending()));

record Product(string Name, string Category, decimal Price);
Output
Electronics Laptop       999
Electronics Monitor      299
Electronics Headphones   149
Furniture   Desk         249
Furniture   Chair        129
61, 72, 88, 95
95, 88, 72, 61

Getting single elements

C#
int[] numbers = [4, 9, 16, 25, 36];

Console.WriteLine(numbers.First());
Console.WriteLine(numbers.First(n => n > 10));
Console.WriteLine(numbers.Last());
Console.WriteLine(numbers.FirstOrDefault(n => n > 100));
Console.WriteLine(numbers.FirstOrDefault(n => n > 100, -1));
Console.WriteLine(numbers.Single(n => n % 5 == 0));
Console.WriteLine(numbers.ElementAt(2));

Console.WriteLine(numbers.First(n => n > 100));
Output
4
16
36
0
-1
25
16
Unhandled exception. System.InvalidOperationException: Sequence contains no matching element

First throws if nothing matches; FirstOrDefault returns the type's default (or a value you supply). Single also throws if more than one element matches — use it when exactly one is expected, such as looking up by a unique ID.

Testing and counting

C#
int[] ages = [34, 17, 52, 29, 41];

Console.WriteLine(ages.Any());
Console.WriteLine(ages.Any(a => a < 18));
Console.WriteLine(ages.All(a => a > 0));
Console.WriteLine(ages.Contains(29));
Console.WriteLine(ages.Count());
Console.WriteLine(ages.Count(a => a >= 30));
Output
True
True
True
True
5
3

Any() is the right way to ask "is it non-empty?" for a general sequence — it stops at the first element, while Count() > 0 may count everything.

Aggregates

C#
List<Product> products = [new("Laptop", 999m, 5), new("Desk", 249m, 3), new("Lamp", 39m, 20), new("Chair", 129m, 12)];

Console.WriteLine(products.Sum(p => p.Stock));
Console.WriteLine(products.Average(p => p.Price));
Console.WriteLine(products.Min(p => p.Price));
Console.WriteLine(products.Max(p => p.Price));
Console.WriteLine(products.MaxBy(p => p.Price * p.Stock));
Console.WriteLine(products.MinBy(p => p.Stock)?.Name);

record Product(string Name, decimal Price, int Stock);
Output
40
354
39
999
Product { Name = Laptop, Price = 999, Stock = 5 }
Desk

Max returns the largest value; MaxBy returns the element with the largest value.

Paging, de-duplicating and converting

C#
int[] data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];

Console.WriteLine(string.Join(" ", data.Take(3)));
Console.WriteLine(string.Join(" ", data.Skip(8)));
Console.WriteLine(string.Join(" ", data.TakeWhile(n => n < 5)));
Console.WriteLine(string.Join(" ", data.Distinct()));
Console.WriteLine(string.Join(" ", data.Reverse()));

List<int> asList = data.Where(n => n > 4).ToList();
int[] asArray = data.Distinct().Order().ToArray();
Dictionary<int, int> squares = asArray.ToDictionary(n => n, n => n * n);
Console.WriteLine($"{asList.Count} {asArray.Length} {squares[9]}");
Output
3 1 4
5 3 5
3 1 4 1
3 1 4 5 9 2 6
5 3 5 6 2 9 5 1 4 1 3
5 7 81

Deferred execution

Most LINQ methods don't do any work when you call them. They build a recipe, which runs only when you enumerate the result — with foreach, or a method like ToList, Count or Sum. And it runs again each time:

C#
List<int> numbers = [1, 2, 3];
var query = numbers.Select(n =>
{
    Console.WriteLine($"  processing {n}");
    return n * 2;
});

Console.WriteLine("Query created");
numbers.Add(4);

Console.WriteLine("First enumeration:");
Console.WriteLine(string.Join(", ", query));

var snapshot = query.ToList();
Console.WriteLine($"Snapshot has {snapshot.Count} items");
Output
Query created
First enumeration:
  processing 1
  processing 2
  processing 3
  processing 4
2, 4, 6, 8
  processing 1
  processing 2
  processing 3
  processing 4
Snapshot has 4 items

Notice the query saw the 4 added after it was created, and ran twice. When you'll use a result more than once — or the work is expensive — call ToList() or ToArray() once and reuse that.

Query syntax

LINQ also has an SQL-like query syntax. The compiler translates it into the same method calls, so use whichever reads better. Query syntax is often clearer for joins and groupings (next lesson):

C#
string[] words = ["banana", "kiwi", "apple", "cherry", "fig", "grape"];

var query =
    from w in words
    where w.Length > 4
    orderby w.Length, w
    select w.ToUpper();

var methods = words.Where(w => w.Length > 4).OrderBy(w => w.Length).ThenBy(w => w).Select(w => w.ToUpper());

Console.WriteLine(string.Join(", ", query));
Console.WriteLine(query.SequenceEqual(methods));
Output
APPLE, GRAPE, BANANA, CHERRY
True

Try it yourself

Using the student list, find the top three scores, the class average, and the names of anyone who failed (below 50).

Try it yourself · Program.cs
Edit the code, copy it into Program.cs and run it with dotnet run Program.cs (.NET 10 or later). How to set up .NET
Output
Top 3: Chloe (91), Asha (82), Finn (75)
Average: 67.0
Failed: Ben, Ella