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
.csfile directly usingdotnet 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:
- // LINQ basics: Where, Select, OrderBy, First/FirstOrDefault, Any, All, Count
- int[] scores = { 88, 92, 75, 64, 99, 81, 70, 95 };
- var passed = scores.Where(s => s >= 75);
- Console.WriteLine("Passed: " + string.Join(", ", passed));
- var sorted = scores.OrderByDescending(s => s);
- Console.WriteLine("Highest to lowest: " + string.Join(", ", sorted));
- var curved = scores.Select(s => Math.Min(s + 5, 100));
- Console.WriteLine("Curved (+5, max 100): " + string.Join(", ", curved));
- Console.WriteLine("First score above 90: " + scores.First(s => s > 90));
- Console.WriteLine("First score above 100 (or 0): " + scores.FirstOrDefault(s => s > 100));
- Console.WriteLine("Any failing score? " + scores.Any(s => s < 75));
- Console.WriteLine("All scores above 60? " + scores.All(s => s > 60));
- Console.WriteLine("Number of scores 90 and up: " + scores.Count(s => s >= 90));
- 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:
- // LINQ with a list of objects: filtering, sorting by two keys, projecting
- {
- };
- // Dean's listers: GWA of 1.75 or better (lower is better in PH grading)
- var deansList = students
- .Where(s => s.Gwa <= 1.75m)
- .OrderBy(s => s.Gwa)
- .ThenBy(s => s.Name)
- .Select(s => $"{s.Name} ({s.Course}) - {s.Gwa}");
- Console.WriteLine("Dean's List:");
- foreach (var line in deansList)
- Console.WriteLine(" " + line);
- // Query syntax does the same thing with SQL-like keywords
- var bsit = from s in students
- where s.Course == "BSIT"
- orderby s.Name descending
- select s.Name;
- Console.WriteLine("BSIT students (Z-A): " + string.Join(", ", bsit));
- // Projection into an anonymous type
- var cards = students.Take(2).Select(s => new { s.Id, Initials = string.Concat(s.Name.Split(' ').Select(p => p[0])) });
- foreach (var c in cards)
- Console.WriteLine($"ID {c.Id}: {c.Initials}");
- 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. CallingOrderBytwice 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. Selectcan create an anonymous type likenew { 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:
- // GroupBy, Join and aggregate functions
- {
- };
- {
- };
- // 1) GroupBy: count products and average price per category
- Console.WriteLine("== Products per category");
- var byCategory = products
- .GroupBy(p => p.Category)
- foreach (var g in byCategory)
- Console.WriteLine($"{g.Category}: {g.Count} items, average price {g.Average:F2}");
- // 2) Join: connect each order to its product (like SQL INNER JOIN)
- Console.WriteLine("== Order lines");
- var lines = orders.Join(products,
- o => o.ProductId,
- p => p.Id,
- foreach (var l in lines)
- Console.WriteLine($"#{l.OrderId} {l.Name} x{l.Quantity} = {l.Total:F2}");
- // 3) Join + GroupBy + Sum: sales per category, highest first
- Console.WriteLine("== Sales per category");
- var sales = lines
- .GroupBy(l => l.Category)
- .OrderByDescending(x => x.Sales);
- foreach (var s in sales)
- Console.WriteLine($"{s.Category}: {s.Sales:F2}");
- // 4) Best-selling product by quantity using MaxBy (.NET 6+)
- var best = lines
- .GroupBy(l => l.Name)
- .MaxBy(x => x.Qty);
- Console.WriteLine($"Best seller: {best!.Name} ({best.Qty} pcs)");
- record Product(int Id, string Name, string Category, decimal Price);
- 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:
- // Deferred execution: a LINQ query runs when you loop over it, not when you write it
- var evens = numbers.Where(n => n % 2 == 0); // not executed yet
- var evensNow = numbers.Where(n => n % 2 == 0).ToList(); // executed immediately
- numbers.Add(4);
- numbers.Add(6);
- Console.WriteLine("Deferred query: " + string.Join(", ", evens));
- Console.WriteLine("ToList() snapshot: " + string.Join(", ", evensNow));
- // Paging with Skip and Take, a common pattern for tables and APIs
- var items = Enumerable.Range(1, 23).Select(i => $"Item {i}").ToList();
- int pageSize = 10;
- int totalPages = (int)Math.Ceiling(items.Count / (double)pageSize);
- for (int page = 1; page <= totalPages; page++)
- {
- var pageItems = items.Skip((page - 1) * pageSize).Take(pageSize).ToList();
- Console.WriteLine($"Page {page}/{totalPages}: {pageItems.First()} to {pageItems.Last()} ({pageItems.Count} items)");
- }
- // Chunk (.NET 6+) splits a sequence into fixed-size groups
- 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. UseFirstOrDefault()and check for the default value. - Calling
Count() > 0to 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
OrderBytwice. UseThenByfor 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!