C# Fundamentals: From Your First Program to Async and JSON · C# Basics: Types, Collections, Classes and Pattern Matching
Collections and LINQ
Use List, Dictionary and HashSet, and query data declaratively with LINQ: Where, Select, OrderBy, GroupBy, Join and aggregation.
What you'll build
An order report. You'll load orders into collections, filter and sort them with LINQ, group sales by customer and country, join orders to customers, and compute totals.
The idea
The core collections live in System.Collections.Generic:
List<T>: a resizable array (Add,Remove,Contains, indexing).Dictionary<TKey, TValue>: key/value lookup. UseTryGetValueto read safely.HashSet<T>: unique items with fastContains.- Collection expressions (C# 12):
List<int> a = [1, 2, 3];and spread[..a, 4].
LINQ (Language Integrated Query) is a set of extension methods on IEnumerable<T> for querying any sequence:
Wherefilters,Selecttransforms,OrderBy/ThenBysort.GroupBygroups,Joincombines two sequences by key.Sum,Average,Min,Max,Count,Any,All,First/FirstOrDefault.ToList(),ToArray(),ToDictionary()materialize results.
LINQ is lazy: a query is not executed until you enumerate it (foreach, ToList(), Count()). Enumerating a query twice runs it twice, so call ToList() when you need a snapshot.
The code
var customers = new List<Customer>
{
new(1, "Ada", "UK"),
new(2, "Grace", "US"),
new(3, "Linus", "FI"),
};
var orders = new List<Order>
{
new(100, 1, 250.00m), new(101, 2, 99.50m), new(102, 1, 40.00m),
new(103, 3, 610.25m), new(104, 2, 15.75m), new(105, 1, 120.00m),
};
// Filter, sort and project
var bigOrders = orders.Where(o => o.Amount >= 100).OrderByDescending(o => o.Amount).Select(o => o.Id).ToList();
Console.WriteLine("big orders: " + string.Join(", ", bigOrders));
// Aggregates
Console.WriteLine($"total {orders.Sum(o => o.Amount):N2}, average {orders.Average(o => o.Amount):N2}, max {orders.Max(o => o.Amount):N2}");
// Join orders to customers, then group by customer
var perCustomer = orders
.Join(customers, o => o.CustomerId, c => c.Id, (o, c) => new { c.Name, c.Country, o.Amount })
.GroupBy(x => x.Name)
.Select(g => new { Name = g.Key, Orders = g.Count(), Total = g.Sum(x => x.Amount) })
.OrderByDescending(x => x.Total);
foreach (var row in perCustomer)
Console.WriteLine($"{row.Name,-6} {row.Orders} orders, total {row.Total,8:N2}");
// Any / All / FirstOrDefault
Console.WriteLine("any order over 600? " + orders.Any(o => o.Amount > 600));
Console.WriteLine("all orders positive? " + orders.All(o => o.Amount > 0));
Console.WriteLine("first order for customer 9: " + (orders.FirstOrDefault(o => o.CustomerId == 9)?.Id.ToString() ?? "none"));
// Dictionary and HashSet
var byId = customers.ToDictionary(c => c.Id);
if (byId.TryGetValue(3, out var found)) Console.WriteLine($"customer 3 is {found.Name}");
var countries = orders.Join(customers, o => o.CustomerId, c => c.Id, (_, c) => c.Country).ToHashSet();
Console.WriteLine("countries: " + string.Join(", ", countries.OrderBy(c => c)));
// LINQ is lazy: the query below sees items added after it was written
var evens = orders.Where(o => o.Id % 2 == 0);
orders.Add(new Order(106, 3, 5.00m));
Console.WriteLine("even-numbered orders: " + evens.Count());
record Customer(int Id, string Name, string Country);
record Order(int Id, int CustomerId, decimal Amount);
Output
big orders: 103, 100, 105 total 1,135.50, average 189.25, max 610.25 Linus 1 orders, total 610.25 Ada 3 orders, total 410.00 Grace 2 orders, total 115.25 any order over 600? True all orders positive? True first order for customer 9: none customer 3 is Linus countries: FI, UK, US even-numbered orders: 4
Run this on your computer — see “Get Started” for setup.
The last two lines show LINQ's laziness: evens was defined before order 106 was added, but it is evaluated when Count() runs, so the new order is included.
Common mistakes
- Enumerating a query repeatedly. Each
foreach,Count()orFirst()re-runs it. CallToList()when you need one stable result. - Using
First()on possibly empty sequences. It throws; useFirstOrDefault()and handlenull. - Modifying a list while a query over it is being enumerated. This throws or yields confusing results.
- Doing database or heavy work in
Wherelambdas on in-memory collections. Filter early and keep lambdas cheap. - Using
Count() > 0whenAny()expresses intent and can stop early.
Key points
List<T>,Dictionary<TKey,TValue>andHashSet<T>cover most needs; collection expressions ([1, 2, 3]) build them concisely.- LINQ queries are lazy chains of
Where,Select,OrderBy,GroupByandJoin. - Materialize with
ToList(),ToArray()orToDictionary()when you need a snapshot. - Use
TryGetValueandFirstOrDefaultto handle missing data safely.
Further reading: LINQ overview.
Practice questions
What does it mean that LINQ queries are lazy?
Answer
A query defined with Where or Select isn't executed when it is declared; it runs each time you enumerate it (foreach, ToList, Count). It therefore sees the data as it is at that moment, and enumerating twice runs it twice.
Why prefer `FirstOrDefault()` to `First()` when a match might not exist?
Answer
First() throws InvalidOperationException if the sequence is empty or nothing matches. FirstOrDefault() returns null (or the type's default) so you can handle absence without an exception.
Open in the app to mark lessons complete, take notes and track your progress.