Site

LINQ — C# Query Language

Tutorial S6  •  C# / Learn

S6.0 What This Teaches

This tutorial covers LINQ (Language Integrated Query) in C#:

S6.1 Where and Select

var numbers = Enumerable.Range(1, 20);

// Where - filter
var evens = numbers.Where(n => n % 2 == 0);

// Select - transform (map)
var squares = numbers.Select(n => n * n);

// Chain operations
var result = numbers
    .Where(n => n % 3 == 0)
    .Select(n => n * n)
    .ToList();   // [9, 36, 81, 144, 225, 324]

// Flatten with SelectMany
var words = new[] { "hello world", "foo bar" };
var allWords = words.SelectMany(s => s.Split(' '));  // ["hello","world","foo","bar"]

S6.2 Ordering and Distinct

var names = new[] { "Charlie", "Alice", "Bob", "Dave", "Alice" };

var sorted    = names.OrderBy(n => n);
var desc      = names.OrderByDescending(n => n);
var unique    = names.Distinct();
var byLength  = names.OrderBy(n => n.Length).ThenBy(n => n);

// Take and Skip for pagination
var page2 = names.OrderBy(n => n).Skip(2).Take(2);

Console.WriteLine(string.Join(", ", sorted));   // Alice, Alice, Bob, Charlie, Dave

S6.3 Aggregation

int[] nums = { 3, 1, 4, 1, 5, 9, 2, 6 };

Console.WriteLine(nums.Sum());          // 31
Console.WriteLine(nums.Min());          // 1
Console.WriteLine(nums.Max());          // 9
Console.WriteLine(nums.Average());      // 3.875
Console.WriteLine(nums.Count());        // 8

// Aggregate applies a function to running total
int product = nums.Aggregate((acc, n) => acc * n);

// First/Last/Single
int first  = nums.First(n => n > 4);    // 5
int? found = nums.FirstOrDefault(n => n > 10);  // null (not found)

S6.4 GroupBy

var words = new[] { "apple", "ant", "bat", "bear", "cherry", "cat" };

var byLetter = words.GroupBy(w => w[0]);
foreach (var group in byLetter.OrderBy(g => g.Key))
{
    Console.WriteLine($"{group.Key}: {string.Join(", ", group)}");
}

// ToLookup: like GroupBy but materializes immediately (multi-dict)
var lookup = words.ToLookup(w => w.Length);
foreach (string w in lookup[3])   // all 3-letter words
    Console.Write($"{w} ");

S6.5 Query Syntax

C# supports SQL-like query syntax as an alternative to method calls. Both compile to identical IL:
int[] nums = { 3, 1, 4, 1, 5, 9, 2, 6 };

// Method syntax
var result1 = nums.Where(n => n > 3).OrderBy(n => n).Select(n => n * 2);

// Query syntax
var result2 = from n in nums
              where n > 3
              orderby n
              select n * 2;

Console.WriteLine(string.Join(", ", result1));  // 8, 8, 10, 12, 18
Method syntax is more common in modern C# and supports operations without query-syntax equivalents (like GroupBy with projections).

S6.6 Deferred Execution

LINQ queries are lazy - they don't execute until iterated. Call ToList(), ToArray(), or ToDictionary() to force immediate evaluation:
var data = new List<int> { 1, 2, 3 };
var query = data.Where(n => n > 1);   // not executed yet

data.Add(4);                            // modify source

foreach (int n in query)               // executes NOW - sees the modified list
    Console.Write($"{n} ");            // 2 3 4

// Force immediate evaluation
var snapshot = data.Where(n => n > 1).ToList();
data.Clear();
Console.WriteLine(snapshot.Count);     // 3 - snapshot is independent

S6.7 Example - All Together

// LINQ - analyze a list of student records.

var students = new[]
{
    (Name: "Alice",  Score: 92, Grade: "A"),
    (Name: "Bob",    Score: 78, Grade: "C"),
    (Name: "Carol",  Score: 95, Grade: "A"),
    (Name: "Dave",   Score: 83, Grade: "B"),
    (Name: "Eve",    Score: 78, Grade: "C"),
};

var avgByGrade = students
    .GroupBy(s => s.Grade)
    .OrderBy(g => g.Key)
    .Select(g => (Grade: g.Key, Avg: g.Average(s => s.Score)));

foreach (var (grade, avg) in avgByGrade)
    Console.WriteLine($"Grade {grade}: avg={avg:F1}");

Console.WriteLine($"Top scorer: {students.MaxBy(s => s.Score)?.Name}");
Expected output:
Grade A: avg=93.5
Grade B: avg=83.0
Grade C: avg=78.0
Top scorer: Carol

S6.8 Exercise

Exercise
  • Given a list of words, use LINQ to find all words longer than 4 characters, convert them to uppercase, sort alphabetically, and print them.
  • Use GroupBy to group integers 1-30 into three buckets: divisible by 3, divisible by 5, or divisible by 15. Print each group.
  • Write a LINQ query that joins two lists — student names and their scores — and produces a sorted ranking. Use Zip to pair the lists.

S6.9 Common Mistakes

Iterating a query multiple times

Each iteration re-executes the query. If the source is expensive (a database query, a file read), call ToList() once and iterate the result.

Using First() on an empty sequence

int val = empty.First();           // InvalidOperationException
int val = empty.FirstOrDefault();  // 0 (default for int) - safe

Mixing query and method syntax unnecessarily

Choosing one style and sticking with it per query is more readable than switching back and forth. Method syntax handles all LINQ operations; query syntax handles some but not all.

S6.10 Key Terms

TermMeaning
LINQLanguage Integrated Query; extension methods on IEnumerable<T>
WhereFilters elements by a predicate
SelectProjects each element to a new form (map)
SelectManyFlattens sequences of sequences (flatMap)
OrderBySorts ascending; ThenBy for secondary sort
GroupByGroups elements by a key; returns IGrouping<K, T>
AggregateApplies an accumulator function (reduce/fold)
deferred executionQuery only runs when iterated; ToList forces immediate evaluation