
Master LINQ in C# with this hands-on C# LINQ tutorial. 10 practical, runnable examples with best practices and pitfalls. Start querying smarter today!
If you write C# for a living, LINQ is not optional — it is the standard way to filter, transform, and aggregate data in .NET. This C# LINQ tutorial walks you through 10 practical examples every developer must know, from simple filtering with Where to grouping, joining, and flattening collections. More importantly, we explain why each operator works the way it does, so you understand deferred execution, avoid the classic multiple-enumeration trap, and write queries that are both readable and fast.
All examples run on modern .NET (6, 8, or 10) and use this shared sample data, so you can paste them straight into a console app or dotnet run a top-level program:
public record Employee(int Id, string Name, string Department, decimal Salary, int Age);
List<Employee> employees = new()
{
new(1, "Alice", "Engineering", 95000m, 34),
new(2, "Bob", "Engineering", 82000m, 28),
new(3, "Carol", "Marketing", 67000m, 41),
new(4, "Dave", "Sales", 58000m, 25),
new(5, "Erin", "Engineering", 110000m, 39),
new(6, "Frank", "Marketing", 72000m, 30),
new(7, "Grace", "Sales", 61000m, 45),
};
What Is LINQ in C# and Why Should You Care?
LINQ (Language Integrated Query) is a set of extension methods on IEnumerable<T> and IQueryable<T> that lets you query collections, databases, XML, and more using one consistent syntax. Instead of writing a foreach loop with an if and a temporary list, you declare what you want and let the runtime figure out how to get it.
The why matters here: declarative queries are easier to review, harder to get wrong (no off-by-one index bugs, no forgotten Add calls), and they compose. A Where followed by a Select followed by an OrderBy reads like a sentence. With Entity Framework Core, the same query shape even translates to SQL — one mental model for in-memory and database data.
C# LINQ Tutorial: 10 Practical Examples
1. Filtering with Where
Where is the operator you will use most. It keeps only elements matching a predicate:
var engineers = employees.Where(e => e.Department == "Engineering");
foreach (var e in engineers)
Console.WriteLine($"{e.Name}: {e.Salary:C0}");
// Alice: $95,000 Bob: $82,000 Erin: $110,000
Why it matters: Where is lazily evaluated. The line above does no work — filtering happens only when you enumerate. That means you can build queries conditionally (add filters inside if blocks) without paying any cost until the final loop or ToList().
2. Projecting with Select
Select transforms each element into a new shape — often an anonymous type or a DTO:
var summaries = employees.Select(e => new { e.Name, Tax = e.Salary * 0.25m });
foreach (var s in summaries)
Console.WriteLine($"{s.Name} owes {s.Tax:C0}");
Why it matters: projecting early keeps only the data you need in memory. Against a database, Select becomes the SQL column list — selecting three columns instead of thirty is one of the cheapest performance wins available.
3. Sorting with OrderBy and ThenBy
var ranked = employees
.OrderBy(e => e.Department)
.ThenByDescending(e => e.Salary);
Common pitfall: chaining two OrderBy calls instead of OrderBy + ThenBy. The second OrderBy completely re-sorts the sequence and silently discards your first sort key. ThenBy adds a tiebreaker; OrderBy starts over.
4. Grouping with GroupBy
Grouping is where LINQ starts replacing serious loop code:
var byDepartment = employees
.GroupBy(e => e.Department)
.Select(g => new
{
Department = g.Key,
Count = g.Count(),
AverageSalary = g.Average(e => e.Salary)
});
foreach (var d in byDepartment)
Console.WriteLine($"{d.Department}: {d.Count} people, avg {d.AverageSalary:C0}");
// Engineering: 3 people, avg $95,667
// Marketing: 2 people, avg $69,500
// Sales: 2 people, avg $59,500
Why it matters: the hand-rolled equivalent needs a Dictionary<string, List<Employee>>, a TryGetValue dance, and a second loop for the averages. GroupBy collapses all of that into one readable expression, and in EF Core it translates to GROUP BY in SQL.
5. Aggregating with Sum, Average, Min, Max, and Count
decimal payroll = employees.Sum(e => e.Salary); // 545,000
decimal topSalary = employees.Max(e => e.Salary); // 110,000
int seniors = employees.Count(e => e.Age >= 40); // 2
var oldest = employees.MaxBy(e => e.Age); // Grace
Best practice: since .NET 6, prefer MaxBy/MinBy when you want the element rather than the value. The old idiom — OrderByDescending(e => e.Age).First() — sorts the entire sequence (O(n log n)) just to grab one item; MaxBy does it in a single O(n) pass.
6. Picking Single Elements: First, FirstOrDefault, Single
var bob = employees.First(e => e.Name == "Bob"); // throws if none
var nobody = employees.FirstOrDefault(e => e.Name == "Zed"); // null if none
var byId = employees.Single(e => e.Id == 3); // throws if 0 or 2+
Why the distinction matters: these operators encode your expectations. Use Single when duplicates would indicate corrupt data — you want the exception, loudly and early. Use FirstOrDefault only when "not found" is a legitimate case you actually handle; otherwise you just trade a clear exception now for a confusing NullReferenceException three method calls later.
7. Existence Checks with Any and All
bool hasHighEarners = employees.Any(e => e.Salary > 100000m); // true
bool allAdults = employees.All(e => e.Age >= 18); // true
bool teamIsEmpty = !employees.Any(); // false
Common pitfall: writing employees.Count(e => e.Salary > 100000m) > 0. That counts every match in the whole collection; Any stops at the first hit. On a database query, the difference is SELECT COUNT(*) versus EXISTS — potentially a full scan versus an index probe.
8. Flattening Nested Collections with SelectMany
SelectMany is the operator most developers learn last but need most often — it flattens one-to-many relationships:
var teams = new[]
{
new { Name = "Platform", Members = new[] { "Alice", "Bob" } },
new { Name = "Growth", Members = new[] { "Carol", "Dave", "Erin" } },
};
var roster = teams.SelectMany(
t => t.Members,
(team, member) => $"{member} ({team.Name})");
// Alice (Platform), Bob (Platform), Carol (Growth), Dave (Growth), Erin (Growth)
Why it matters: Select here would give you an IEnumerable<string[]> — a sequence of arrays. SelectMany gives you one flat IEnumerable<string>. Any time you catch yourself writing nested foreach loops to build a flat list, SelectMany is the answer.
9. Joining Two Sequences with Join
var departments = new[]
{
new { Name = "Engineering", Location = "Austin" },
new { Name = "Marketing", Location = "London" },
new { Name = "Sales", Location = "Toronto" },
};
var directory = employees.Join(
departments,
e => e.Department, // outer key
d => d.Name, // inner key
(e, d) => $"{e.Name} works in {d.Location}");
Why it matters: Join builds a hash lookup of the inner sequence, so matching is O(n + m). The naive alternative — a Where inside a Select scanning the second list per element — is O(n × m). With 10,000 employees and 1,000 departments, that is the difference between eleven thousand operations and ten million.
10. Paging with Distinct, Skip, and Take
var uniqueDepartments = employees
.Select(e => e.Department)
.Distinct()
.Order(); // Engineering, Marketing, Sales
int pageSize = 3, page = 2;
var secondPage = employees
.OrderBy(e => e.Id) // ALWAYS order before paging
.Skip((page - 1) * pageSize)
.Take(pageSize); // Dave, Erin, Frank
Common pitfall: paging an unordered sequence. In-memory lists happen to keep insertion order, but a SQL database makes no ordering guarantee without ORDER BY — users can see the same row on page 2 and page 3, or miss rows entirely. Always sort by a stable, unique key before Skip/Take.
LINQ Best Practices and Deferred Execution Explained
The single most important concept in this C# LINQ tutorial is deferred execution. Operators like Where, Select, and OrderBy build a query; nothing runs until something enumerates it — a foreach, ToList(), Count(), or First(). This has three practical consequences:
- Multiple enumeration re-runs the query. If you call
Count()and then loop over the sameIEnumerable, the filter executes twice. Against a database, that is two round trips. If you need the results more than once, materialize them withToList()once, then reuse the list. - Queries capture variables, not values. A lambda like
e => e.Salary > thresholdreadsthresholdat enumeration time. Change the variable after building the query and you change the results — a genuinely confusing bug the first time it bites you. - Exceptions surface late. A bad predicate throws when enumerated, not when declared, so the stack trace points at the
foreach, far from the buggy lambda.
A few more best practices worth internalizing:
- Don't call
ToList()reflexively. Materializing between every operator allocates intermediate lists and defeats laziness. Chain operators and materialize once at the end — or not at all if a singleforeachconsumes the result. - Filter before you project, and filter in the database. With EF Core, keep
Whereclauses translatable so filtering happens in SQL. CallingToList()and thenWheredrags the whole table into memory first. - Prefer clarity over cleverness. A three-operator chain beats a one-line
Aggregatenobody on your team can read. If a query grows past four or five operators, extract named intermediate variables — deferred execution makes this free. - LINQ vs foreach: a plain loop is still fine — sometimes faster — for hot paths and simple mutations. LINQ's win is correctness and readability for querying; it is not a religion.
Conclusion: Key Takeaways from This C# LINQ Tutorial
You have now seen the 10 LINQ operations that cover the overwhelming majority of real-world C# code: Where, Select, OrderBy/ThenBy, GroupBy, the aggregates, element operators like First and Single, Any/All, SelectMany, Join, and Skip/Take paging. The key takeaways from this C# LINQ tutorial:
- LINQ queries are lazy — they run when enumerated, and re-run each time. Materialize with
ToList()exactly once when you need reuse. - Choose operators that encode intent:
AnyoverCount() > 0,Singlewhen duplicates are a bug,MaxByover sort-then-first. SelectManyreplaces nested loops;Joinreplaces O(n × m) scans;ThenBy— not a secondOrderBy— adds sort tiebreakers.- Always order before paging, and let the database do the filtering when you're using EF Core.
The best way to make these stick is to refactor one real loop in your codebase into a LINQ chain today. Then explore the next level: ToLookup, Zip, Chunk, and custom operators with yield return — all built on the same foundations you just learned.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment