A huge amount of programming is working with collections: find the orders over £100, list customer names in alphabetical order, total the sales for each month, check whether any item is out of stock. Written with loops, each task takes several lines of bookkeeping. LINQ (Language Integrated Query) lets you express them directly: orders.Where(o => o.Total > 100).OrderBy(o => o.Date).
LINQ is a set of methods that work on any IEnumerable<T>: arrays, lists, dictionaries, sets, lines of a file, and results from a database. Each method takes a lambda, a small inline function, that says how to filter, transform or sort the items. Methods return new sequences, so they chain together into readable pipelines.
LINQ is one of the features C# developers use most every day. In this lesson you'll filter, transform, sort, group, join and summarise data, learn which methods return one item and which return many, and understand deferred execution, which explains some of LINQ's surprising behaviour.
Filtering, projecting and sorting
var products = new List<Product>
{
new("Mug", "Kitchen", 8.50m, 12),
new("Lamp", "Home", 24.00m, 0),
new("Kettle", "Kitchen", 29.99m, 5)
};
var names = products
.Where(p => p.Stock > 0) // keep matching items
.OrderByDescending(p => p.Price) // sort
.Select(p => p.Name) // transform each item
.ToList(); // run the query and store the resultWherekeeps items for which the lambda returnstrue.Selectturns each item into something else, such as one property or a new anonymous object.OrderByandOrderByDescendingsort;ThenByadds a second sort key.Take(n),Skip(n),Distinct()andChunk(size)are useful for paging and cleaning data.
Single items and summaries
| Method | Returns |
|---|---|
First(predicate) | The first match, or throws if none |
FirstOrDefault(predicate) | The first match, or null/default if none |
Single(predicate) | The only match; throws if there are zero or several |
Any(predicate) / All(predicate) | true or false |
Count(), Sum(), Average(), Min(), Max() | A number |
MaxBy(selector) / MinBy(selector) | The item with the largest or smallest value |
Any stops at the first match, so list.Any() is the fastest way to check that a sequence isn't empty.
Grouping and joining
GroupBy collects items that share a key. Each group has a Key and contains its items:
var byCategory = products
.GroupBy(p => p.Category)
.Select(g => new { Category = g.Key, Count = g.Count(), Value = g.Sum(p => p.Price * p.Stock) });Join matches items from two sequences by a shared key, like a SQL join. SelectMany flattens nested collections, such as all the items from every order. LINQ also has a query syntax that reads like SQL: from p in products where p.Stock > 0 select p.Name. It compiles to the same method calls, so use whichever reads better.
Deferred execution
Most LINQ methods don't run immediately. products.Where(...) creates a query that runs only when you loop over it or call a method like ToList(), Count() or First(). If the source changes in between, the query sees the change, and running it twice does the work twice. Call ToList() or ToArray() when you want to capture results once.
Turning query results into other collections
ToList() and ToArray() are the usual ways to run a query, but LINQ can also build other collections directly:
var byName = products.ToDictionary(p => p.Name, p => p.Price);
decimal lampPrice = byName["Lamp"];
var categories = products.Select(p => p.Category).ToHashSet();
var byCategory = products.ToLookup(p => p.Category);
foreach (var p in byCategory["Kitchen"])
Console.WriteLine(p.Name);ToDictionary(keySelector, valueSelector)throws anArgumentExceptionif two items produce the same key, so only use it when keys are unique, such as ids.ToHashSet()keeps one copy of each value, which is handy for fast "is this present?" checks later.ToLookupis like a dictionary where every key maps to a group of items. Unlike a dictionary, asking for a missing key returns an empty sequence instead of throwing.
For a calculation that LINQ doesn't have a named method for, Aggregate combines all items into one result by applying a function repeatedly. new[] { 1, 2, 3, 4 }.Aggregate((a, b) => a * b) multiplies them together to give 24.
C# Advanced Syntax
LINQ methods
var cheap = items.Where(i => i.Price < 10);
var names = items.Select(i => i.Name);
var sorted = items.OrderBy(i => i.Category).ThenByDescending(i => i.Price);
var first = items.FirstOrDefault(i => i.Id == 3);
bool any = items.Any(i => i.Stock == 0);
decimal total = items.Sum(i => i.Price);
var groups = items.GroupBy(i => i.Category);
var page = items.Skip(20).Take(10);
var lookup = items.ToDictionary(i => i.Id);
var q = from i in items where i.Price > 5 orderby i.Name select i.Name;C# Advanced Summary
- LINQ methods work on any IEnumerable<T> and chain into pipelines.
- Where filters, Select transforms, OrderBy/ThenBy sort.
- First, FirstOrDefault, Single, Any and All return single results; Count, Sum and Average summarise.
- GroupBy groups by a key; Join combines two sequences; SelectMany flattens.
- Queries are deferred until enumerated; ToList captures results.
C# Advanced Examples
Each example shows the code, the output it produces and what each part does.
Filtering and summarising orders
var orders = new List<Order>
{
new(1, "Ada", 120.00m, new DateOnly(2026, 3, 2)),
new(2, "Ben", 35.50m, new DateOnly(2026, 3, 2)),
new(3, "Ada", 64.25m, new DateOnly(2026, 3, 5)),
new(4, "Cy", 210.00m, new DateOnly(2026, 3, 9)),
new(5, "Ben", 18.75m, new DateOnly(2026, 3, 9))
};
var big = orders.Where(o => o.Total >= 60).OrderByDescending(o => o.Total);
Console.WriteLine(string.Join(", ", big.Select(o => $"#{o.Id} {o.Total}")));
Console.WriteLine($"Count: {orders.Count()}, total: {orders.Sum(o => o.Total)}");
Console.WriteLine($"Average: {orders.Average(o => o.Total):F2}");
Console.WriteLine($"Largest: #{orders.MaxBy(o => o.Total)!.Id}");
Console.WriteLine($"Any under 20: {orders.Any(o => o.Total < 20)}");
Console.WriteLine($"Customers: {string.Join(", ", orders.Select(o => o.Customer).Distinct())}");
record Order(int Id, string Customer, decimal Total, DateOnly Date);#4 210.00, #1 120.00, #3 64.25
Count: 5, total: 448.50
Average: 89.70
Largest: #4
Any under 20: True
Customers: Ada, Ben, CyLine-by-line explanation
- Line 10Where filters and OrderByDescending sorts, largest first.
- Line 11Select turns each order into a short label.
- Line 13120 + 35.50 + 64.25 + 210 + 18.75 = 448.50.
- Line 14448.50 divided by 5 is 89.70.
- Line 15MaxBy returns the whole order with the largest total, not just the number.
- Line 17Distinct removes repeated names, keeping the first-seen order.
Grouping by customer
var orders = new[]
{
new { Customer = "Ada", Total = 120.00m },
new { Customer = "Ben", Total = 35.50m },
new { Customer = "Ada", Total = 64.25m },
new { Customer = "Ben", Total = 18.75m },
new { Customer = "Cy", Total = 210.00m }
};
var summary = orders
.GroupBy(o => o.Customer)
.Select(g => new { Name = g.Key, Orders = g.Count(), Spent = g.Sum(o => o.Total) })
.OrderByDescending(x => x.Spent);
foreach (var s in summary)
{
Console.WriteLine($"{s.Name,-4}{s.Orders} orders {s.Spent,8:F2}");
}
var lookup = orders.ToLookup(o => o.Customer);
Console.WriteLine(lookup["Ben"].Count());Cy 1 orders 210.00
Ada 2 orders 184.25
Ben 2 orders 54.25
2Line-by-line explanation
- Lines 1 to 8Anonymous objects hold quick, throwaway data without declaring a type.
- Line 11GroupBy collects the orders that share a customer name.
- Line 12Each group has a Key and can be counted and summed.
- Line 13Sort the summaries by amount spent.
- Lines 20 to 21ToLookup builds a ready-made grouping you can index by key; Ben has two orders.
Join, SelectMany and deferred execution
var customers = new List<(int Id, string Name)> { (1, "Ada"), (2, "Ben") };
var orders = new List<(int CustomerId, string[] Items)>
{
(1, new[] { "Mug", "Lamp" }),
(2, new[] { "Pen" }),
(1, new[] { "Kettle" })
};
var joined = customers.Join(orders,
c => c.Id,
o => o.CustomerId,
(c, o) => $"{c.Name}: {string.Join("/", o.Items)}");
Console.WriteLine(string.Join("; ", joined));
var allItems = orders.SelectMany(o => o.Items).OrderBy(i => i);
Console.WriteLine(string.Join(", ", allItems));
var numbers = new List<int> { 1, 2, 3 };
var evens = numbers.Where(n => n % 2 == 0);
numbers.Add(4);
Console.WriteLine(string.Join(", ", evens));Ada: Mug/Lamp; Ada: Kettle; Ben: Pen
Kettle, Lamp, Mug, Pen
2, 4Line-by-line explanation
- Lines 9 to 12Join matches each customer to their orders by id, then builds a result from each pair.
- Line 15SelectMany flattens the item arrays from every order into one sequence.
- Line 19The query is only defined here, not run.
- Lines 20 to 214 was added before the query ran, so it's included in the results.
Common C# Advanced Mistakes
First on a sequence that might be empty
var order = orders.First(o => o.Id == id); // InvalidOperationException if not foundvar order = orders.FirstOrDefault(o => o.Id == id);
if (order is null) return NotFound();Running the same query many times
var active = users.Where(u => u.IsActive);
Console.WriteLine(active.Count()); // runs the filter
foreach (var u in active) { } // runs it againvar active = users.Where(u => u.IsActive).ToList(); // run once, reuse the listCount() > 0 to check for items
if (orders.Where(o => o.IsLate).Count() > 0) { } // counts every matchif (orders.Any(o => o.IsLate)) { } // stops at the first matchC# Advanced in Real Projects
LINQ in real applications
In ASP.NET Core apps with Entity Framework Core, LINQ queries such as db.Orders.Where(o => o.CustomerId == id).OrderByDescending(o => o.Date).Take(10) are translated into SQL and run in the database. In memory, LINQ shapes API responses, builds reports, removes duplicates from imports and groups log entries. It's so common that reading LINQ fluently is expected of every C# developer.
C# Advanced FAQ
What is the difference between First and FirstOrDefault?
First returns the first matching item and throws an InvalidOperationException if there isn't one. FirstOrDefault returns default instead, which is null for classes and 0 for numbers. Use First when an empty result would be a bug, and FirstOrDefault with a null check when it's a normal possibility.
What is the difference between Select and SelectMany?
Select turns each item into exactly one result, so a list of orders becomes a list of something else with the same count. SelectMany lets each item produce a sequence and flattens all of them into one, so orders.SelectMany(o => o.Items) gives every item from every order in a single list.
Is LINQ slower than a foreach loop?
LINQ adds a little overhead from delegates and iterators, so a hand-written loop can be slightly faster in hot paths. For most application code the difference is negligible, and LINQ's clarity is worth it. Measure before rewriting, and watch for real problems such as running the same query many times.
How do I remove duplicates with LINQ?
Use Distinct() for simple values like numbers and strings. To remove duplicates based on one property, use DistinctBy(p => p.Email), available since .NET 6, which keeps the first item for each key. Records compare by value, so Distinct() works on them directly.
What does ToList do in LINQ?
ToList() runs the query immediately and stores the results in a new List<T>. Without it, most LINQ queries are deferred and run again every time you loop over them. Call it when you need the results more than once or want to capture them at a particular moment.
C# Advanced Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the query to keep only prices above 10.
var expensive = prices.(p => p > 10);
What does this print?
var nums = new[] { 1, 2, 3, 4 };
Console.WriteLine(nums.Select(n => n * 10).Sum());
What does FirstOrDefault return when nothing matches a list of objects?
What does this print?
var words = new[] { "apple", "avocado", "banana", "blueberry", "cherry" };
var groups = words.GroupBy(w => w[0]);
Console.WriteLine(groups.Count());
What does deferred execution mean in LINQ?
C# Advanced Quiz
5 questions to check what you learned.
Which method transforms each item into something else?
- Where
- OrderBy
- Select
- Any
Which method checks whether any item matches a condition?
- All
- Any
- Contains
- Count
What does GroupBy return?
- A dictionary
- A sorted list
- A single number
- A sequence of groups, each with a Key and its items
What does Single throw if two items match?
- An InvalidOperationException
- Nothing, it returns the first
- A FormatException
- A KeyNotFoundException
Which method flattens a list of orders, each with a list of items, into one list of items?
- Select
- GroupBy
- Join
- SelectMany