Skip to main content

C# LINQ Tutorial: 10 Practical Examples You Must Know

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 same IEnumerable, the filter executes twice. Against a database, that is two round trips. If you need the results more than once, materialize them with ToList() once, then reuse the list.
  • Queries capture variables, not values. A lambda like e => e.Salary > threshold reads threshold at 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 single foreach consumes the result.
  • Filter before you project, and filter in the database. With EF Core, keep Where clauses translatable so filtering happens in SQL. Calling ToList() and then Where drags the whole table into memory first.
  • Prefer clarity over cleverness. A three-operator chain beats a one-line Aggregate nobody 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: Any over Count() > 0, Single when duplicates are a bug, MaxBy over sort-then-first.
  • SelectMany replaces nested loops; Join replaces O(n × m) scans; ThenBy — not a second OrderBy — 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.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...