C# LINQ Tutorial with Examples: Where, Select, OrderBy, GroupBy, Join and Sum

Filtering a list, sorting it, computing totals, grouping records by category... these are tasks you do in almost every C# program, from a simple grading app to a full inventory system. Writing them with for loops and temporary lists works, but the code quickly becomes long and hard to read. LINQ (Language Integrated Query) solves this by letting you query collections with short, readable expressions that feel a lot like SQL.

In this tutorial, you will learn LINQ in C# through practical examples: Where, Select, OrderBy, First, Any, GroupBy, Join, Sum, MaxBy, paging with Skip and Take, and the important idea of deferred execution. Every program below was run on .NET 10 (and also checked on .NET 8), and the output shown is the real output.

Requirements

  • .NET SDK 8 or newer. With .NET 10 you can run a single .cs file directly using dotnet run File.cs.
  • Visual Studio 2022/2026, VS Code with the C# Dev Kit, or JetBrains Rider.
  • Basic C# knowledge: variables, lists and lambda expressions (x => x * 2).

If you are on .NET 8 or 9, create a project with dotnet new console -o LinqDemo, replace the contents of Program.cs with an example, and run dotnet run. LINQ lives in the System.Linq namespace, which is already included by the default implicit usings.

Step 1: LINQ Basics with a Simple Array

Let's start with an array of exam scores. Create LinqBasics.cs:

  1. // LINQ basics: Where, Select, OrderBy, First/FirstOrDefault, Any, All, Count
  2. int[] scores = { 88, 92, 75, 64, 99, 81, 70, 95 };
  3.  
  4. var passed = scores.Where(s => s >= 75);
  5. Console.WriteLine("Passed: " + string.Join(", ", passed));
  6.  
  7. var sorted = scores.OrderByDescending(s => s);
  8. Console.WriteLine("Highest to lowest: " + string.Join(", ", sorted));
  9.  
  10. var curved = scores.Select(s => Math.Min(s + 5, 100));
  11. Console.WriteLine("Curved (+5, max 100): " + string.Join(", ", curved));
  12.  
  13. Console.WriteLine("First score above 90: " + scores.First(s => s > 90));
  14. Console.WriteLine("First score above 100 (or 0): " + scores.FirstOrDefault(s => s > 100));
  15. Console.WriteLine("Any failing score? " + scores.Any(s => s < 75));
  16. Console.WriteLine("All scores above 60? " + scores.All(s => s > 60));
  17. Console.WriteLine("Number of scores 90 and up: " + scores.Count(s => s >= 90));
  18. Console.WriteLine($"Average: {scores.Average():F2}, Max: {scores.Max()}, Min: {scores.Min()}");

Run it with dotnet run LinqBasics.cs on .NET 10, or paste it into Program.cs of a console project and run dotnet run. Output:

Passed: 88, 92, 75, 99, 81, 95
Highest to lowest: 99, 95, 92, 88, 81, 75, 70, 64
Curved (+5, max 100): 93, 97, 80, 69, 100, 86, 75, 100
First score above 90: 92
First score above 100 (or 0): 0
Any failing score? True
All scores above 60? True
Number of scores 90 and up: 3
Average: 83.00, Max: 99, Min: 64

Each method takes a lambda that describes what you want. Where keeps items that match a condition, Select transforms each item, and OrderByDescending sorts. Be careful with First(): it throws an InvalidOperationException when nothing matches, while FirstOrDefault() returns the default value (0 for numbers, null for objects). Any and All return a simple true or false, which is perfect for validation checks.

Step 2: Querying a List of Objects

In real applications you work with objects, not plain numbers. This example uses a Student record and finds the Dean's List students. Save it as LinqObjects.cs:

  1. // LINQ with a list of objects: filtering, sorting by two keys, projecting
  2. var students = new List<Student>
  3. {
  4.     new(1, "Ana Santos",   "BSIT", 1.75m),
  5.     new(2, "Ben Cruz",     "BSCS", 2.50m),
  6.     new(3, "Carla Reyes",  "BSIT", 1.25m),
  7.     new(4, "Diego Ramos",  "BSIS", 3.00m),
  8.     new(5, "Ella Mendoza", "BSCS", 1.50m),
  9.     new(6, "Felix Garcia", "BSIT", 2.25m),
  10. };
  11.  
  12. // Dean's listers: GWA of 1.75 or better (lower is better in PH grading)
  13. var deansList = students
  14.     .Where(s => s.Gwa <= 1.75m)
  15.     .OrderBy(s => s.Gwa)
  16.     .ThenBy(s => s.Name)
  17.     .Select(s => $"{s.Name} ({s.Course}) - {s.Gwa}");
  18.  
  19. Console.WriteLine("Dean's List:");
  20. foreach (var line in deansList)
  21.     Console.WriteLine("  " + line);
  22.  
  23. // Query syntax does the same thing with SQL-like keywords
  24. var bsit = from s in students
  25.            where s.Course == "BSIT"
  26.            orderby s.Name descending
  27.            select s.Name;
  28. Console.WriteLine("BSIT students (Z-A): " + string.Join(", ", bsit));
  29.  
  30. // Projection into an anonymous type
  31. var cards = students.Take(2).Select(s => new { s.Id, Initials = string.Concat(s.Name.Split(' ').Select(p => p[0])) });
  32. foreach (var c in cards)
  33.     Console.WriteLine($"ID {c.Id}: {c.Initials}");
  34.  
  35. record Student(int Id, string Name, string Course, decimal Gwa);

Run it with dotnet run LinqObjects.cs on .NET 10, or paste it into Program.cs of a console project and run dotnet run. Output:

Dean's List:
  Carla Reyes (BSIT) - 1.25
  Ella Mendoza (BSCS) - 1.50
  Ana Santos (BSIT) - 1.75
BSIT students (Z-A): Felix Garcia, Carla Reyes, Ana Santos
ID 1: AS
ID 2: BC

A few things to notice:

  • OrderBy(...).ThenBy(...) sorts by a second key when the first key is equal. Calling OrderBy twice would throw away the first sort.
  • The query syntax (from ... where ... select) and the method syntax (.Where().Select()) compile to the same thing. Use whichever reads better; many developers prefer method syntax for short queries and query syntax for complex joins.
  • Select can create an anonymous type like new { s.Id, Initials = ... } when you only need a few fields.

Step 3: GroupBy, Join and Aggregates

This is where LINQ really shines. Imagine a small school supplies store with a product list and an order list. We want the number of products per category, each order line with its total, sales per category, and the best-selling item. Save this as LinqGroupJoin.cs:

  1. // GroupBy, Join and aggregate functions
  2. var products = new List<Product>
  3. {
  4.     new(1, "Ballpen",       "School Supplies", 12.50m),
  5.     new(2, "Notebook",      "School Supplies", 45.00m),
  6.     new(3, "USB Flash 32GB", "Gadgets",        299.00m),
  7.     new(4, "Mouse",         "Gadgets",        350.00m),
  8.     new(5, "Bond Paper",    "School Supplies", 230.00m),
  9. };
  10.  
  11. var orders = new List<Order>
  12. {
  13.     new(101, 1, 10), new(102, 3, 2), new(103, 2, 5),
  14.     new(104, 4, 1),  new(105, 1, 20), new(106, 5, 3),
  15. };
  16.  
  17. // 1) GroupBy: count products and average price per category
  18. Console.WriteLine("== Products per category");
  19. var byCategory = products
  20.     .GroupBy(p => p.Category)
  21.     .Select(g => new { Category = g.Key, Count = g.Count(), Average = g.Average(p => p.Price) });
  22. foreach (var g in byCategory)
  23.     Console.WriteLine($"{g.Category}: {g.Count} items, average price {g.Average:F2}");
  24.  
  25. // 2) Join: connect each order to its product (like SQL INNER JOIN)
  26. Console.WriteLine("== Order lines");
  27. var lines = orders.Join(products,
  28.     o => o.ProductId,
  29.     p => p.Id,
  30.     (o, p) => new { o.OrderId, p.Name, p.Category, o.Quantity, Total = o.Quantity * p.Price });
  31. foreach (var l in lines)
  32.     Console.WriteLine($"#{l.OrderId} {l.Name} x{l.Quantity} = {l.Total:F2}");
  33.  
  34. // 3) Join + GroupBy + Sum: sales per category, highest first
  35. Console.WriteLine("== Sales per category");
  36. var sales = lines
  37.     .GroupBy(l => l.Category)
  38.     .Select(g => new { Category = g.Key, Sales = g.Sum(l => l.Total) })
  39.     .OrderByDescending(x => x.Sales);
  40. foreach (var s in sales)
  41.     Console.WriteLine($"{s.Category}: {s.Sales:F2}");
  42.  
  43. // 4) Best-selling product by quantity using MaxBy (.NET 6+)
  44. var best = lines
  45.     .GroupBy(l => l.Name)
  46.     .Select(g => new { Name = g.Key, Qty = g.Sum(l => l.Quantity) })
  47.     .MaxBy(x => x.Qty);
  48. Console.WriteLine($"Best seller: {best!.Name} ({best.Qty} pcs)");
  49.  
  50. record Product(int Id, string Name, string Category, decimal Price);
  51. record Order(int OrderId, int ProductId, int Quantity);

Run it with dotnet run LinqGroupJoin.cs on .NET 10, or paste it into Program.cs of a console project and run dotnet run. Output:

== Products per category
School Supplies: 3 items, average price 95.83
Gadgets: 2 items, average price 324.50
== Order lines
#101 Ballpen x10 = 125.00
#102 USB Flash 32GB x2 = 598.00
#103 Notebook x5 = 225.00
#104 Mouse x1 = 350.00
#105 Ballpen x20 = 250.00
#106 Bond Paper x3 = 690.00
== Sales per category
School Supplies: 1290.00
Gadgets: 948.00
Best seller: Ballpen (30 pcs)

GroupBy returns groups, and each group has a Key plus the items in it, so you can call Count(), Sum() or Average() per group, just like GROUP BY in SQL. Join takes four arguments: the second collection, the key from the first, the key from the second, and a function that builds the result. It behaves like an SQL INNER JOIN, so orders without a matching product are skipped. Finally, MaxBy (added in .NET 6) returns the whole item with the highest value instead of only the number.

We used decimal for prices because it stores money values exactly. Avoid double for currency, since it can produce rounding errors like 0.30000000000000004.

Step 4: Deferred Execution, Paging and Chunking

One LINQ behavior surprises many beginners: most queries do not run when you write them. They run when you loop over the result, call ToList(), Count() or similar. Save this as LinqDeferred.cs:

  1. // Deferred execution: a LINQ query runs when you loop over it, not when you write it
  2. var numbers = new List<int> { 1, 2, 3 };
  3.  
  4. var evens = numbers.Where(n => n % 2 == 0);      // not executed yet
  5. var evensNow = numbers.Where(n => n % 2 == 0).ToList(); // executed immediately
  6.  
  7. numbers.Add(4);
  8. numbers.Add(6);
  9.  
  10. Console.WriteLine("Deferred query: " + string.Join(", ", evens));
  11. Console.WriteLine("ToList() snapshot: " + string.Join(", ", evensNow));
  12.  
  13. // Paging with Skip and Take, a common pattern for tables and APIs
  14. var items = Enumerable.Range(1, 23).Select(i => $"Item {i}").ToList();
  15. int pageSize = 10;
  16. int totalPages = (int)Math.Ceiling(items.Count / (double)pageSize);
  17. for (int page = 1; page <= totalPages; page++)
  18. {
  19.     var pageItems = items.Skip((page - 1) * pageSize).Take(pageSize).ToList();
  20.     Console.WriteLine($"Page {page}/{totalPages}: {pageItems.First()} to {pageItems.Last()} ({pageItems.Count} items)");
  21. }
  22.  
  23. // Chunk (.NET 6+) splits a sequence into fixed-size groups
  24. foreach (var chunk in new[] { "A", "B", "C", "D", "E" }.Chunk(2))
  25.     Console.WriteLine("Chunk: " + string.Join("", chunk));

Run it with dotnet run LinqDeferred.cs on .NET 10, or paste it into Program.cs of a console project and run dotnet run. Output:

Deferred query: 2, 4, 6
ToList() snapshot: 2
Page 1/3: Item 1 to Item 10 (10 items)
Page 2/3: Item 11 to Item 20 (10 items)
Page 3/3: Item 21 to Item 23 (3 items)
Chunk: AB
Chunk: CD
Chunk: E

The evens query was created when the list held only 1, 2 and 3, but it ran later, after we added 4 and 6, so it found three even numbers. The ToList() version captured a snapshot immediately and found only one. Use ToList() or ToArray() when you want to freeze the results or when you will loop over them several times, so the query is not executed again and again.

The paging pattern Skip((page - 1) * pageSize).Take(pageSize) is the same one you will use with Entity Framework Core to show tables page by page, and Chunk is handy for processing records in batches.

Common LINQ Mistakes

  • Using First() when there may be no match. Use FirstOrDefault() and check for the default value.
  • Calling Count() > 0 to check for items. Any() is clearer and can stop at the first item.
  • Running the same query many times. Because of deferred execution, looping over a query twice runs it twice. Store the result with ToList() if you need it again.
  • Chaining OrderBy twice. Use ThenBy for the second sort key.

Conclusion

LINQ turns many lines of loops and if statements into short, readable queries. With Where, Select, OrderBy, GroupBy, Join and the aggregate methods, you can handle most data tasks in your C# projects, and the same skills carry over directly to Entity Framework Core, where LINQ queries are translated into SQL. Try rewriting a loop from one of your own projects using LINQ as practice. You can download all four example files below.

Happy coding!