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
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));
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:
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);
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:
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);
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:
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);
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
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));
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
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));
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
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);
40
354
39
999
Product { Name = Laptop, Price = 999, Stock = 5 }
DeskMax returns the largest value; MaxBy returns the element with the largest value.
Paging, de-duplicating and converting
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]}");
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:
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");
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):
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));
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).
Top 3: Chloe (91), Asha (82), Finn (75) Average: 67.0 Failed: Ben, Ella