Skip to main content

C# LINQ Tutorial: 10 Practical Examples You Must Know

Master LINQ in C# with 10 practical, runnable examples covering Where, Select, GroupBy, Join, and more. Learn best practices and common pitfalls today.

If you write C# for a living, you use LINQ every single day — whether you realize it or not. This C# LINQ tutorial walks through 10 practical, runnable examples that cover the operations developers actually use in production: filtering, projecting, grouping, joining, aggregating, and paging data. Along the way, we explain why each operator behaves the way it does, so you can avoid the pitfalls (deferred execution, multiple enumeration, N+1 queries) that trip up even experienced .NET developers.

All examples target .NET 8 or later and run as-is in a console app. Let's start with the sample data used throughout.

Sample Data for This C# LINQ Tutorial

We'll use a small product catalog with orders. Copy this into a Program.cs to follow along.

using System;
using System.Collections.Generic;
using System.Linq;

public record Product(int Id, string Name, string Category, decimal Price, int Stock);
public record Order(int Id, int ProductId, int Quantity, DateTime OrderedAt);

public static class SampleData
{
    public static List<Product> Products = new()
    {
        new(1, "Laptop",        "Electronics", 1299.99m, 12),
        new(2, "Mouse",         "Electronics",   24.50m, 150),
        new(3, "Desk Chair",    "Furniture",    189.00m, 8),
        new(4, "Standing Desk", "Furniture",    449.00m, 0),
        new(5, "Notebook",      "Stationery",     3.99m, 500),
        new(6, "Pen Set",       "Stationery",    12.00m, 75),
        new(7, "Monitor",       "Electronics",  329.00m, 20),
    };

    public static List<Order> Orders = new()
    {
        new(101, 1, 2, new DateTime(2026, 8, 1)),
        new(102, 2, 10, new DateTime(2026, 8, 3)),
        new(103, 5, 100, new DateTime(2026, 8, 5)),
        new(104, 7, 3, new DateTime(2026, 8, 10)),
        new(105, 1, 1, new DateTime(2026, 8, 15)),
        new(106, 6, 5, new DateTime(2026, 8, 20)),
    };
}

1. Filtering with Where

Where is the most-used operator in LINQ in C#. It takes a predicate and returns only the elements that match. Note that it returns an IEnumerable<T>, not a list — nothing executes until you enumerate it.

var inStockElectronics = SampleData.Products
    .Where(p => p.Category == "Electronics" && p.Stock > 0);

foreach (var p in inStockElectronics)
    Console.WriteLine($"{p.Name} - ${p.Price}");
// Laptop - $1299.99, Mouse - $24.50, Monitor - $329.00

Why it matters: Because Where is lazy, you can build queries conditionally without paying for intermediate collections:

IEnumerable<Product> query = SampleData.Products;
string? categoryFilter = "Furniture";
decimal? maxPrice = 200m;

if (categoryFilter is not null) query = query.Where(p => p.Category == categoryFilter);
if (maxPrice is not null)       query = query.Where(p => p.Price <= maxPrice);

var results = query.ToList(); // executes once, with both filters applied

2. Projecting with Select

Select transforms each element into a new shape. Use it to return only the data your caller needs — a DTO, an anonymous type, or a single property.

var summaries = SampleData.Products
    .Select(p => new { p.Name, DisplayPrice = p.Price.ToString("C") });

foreach (var s in summaries)
    Console.WriteLine($"{s.Name}: {s.DisplayPrice}");

// Select with index
var numbered = SampleData.Products
    .Select((p, i) => $"{i + 1}. {p.Name}");

When the query runs against Entity Framework Core, Select is translated to SQL, so projecting early means fewer columns pulled from the database. That's one of the easiest performance wins in any C# LINQ query.

3. Sorting with OrderBy and ThenBy

A classic interview question: what's the difference between OrderBy(...).OrderBy(...) and OrderBy(...).ThenBy(...)? The first replaces the sort; the second adds a secondary key.

var sorted = SampleData.Products
    .OrderBy(p => p.Category)
    .ThenByDescending(p => p.Price);

foreach (var p in sorted)
    Console.WriteLine($"{p.Category,-12} {p.Name,-14} {p.Price,8:C}");

LINQ's OrderBy is a stable sort — equal elements keep their original order. List<T>.Sort() is not stable, which is a subtle source of bugs when you switch between them.

4. Grouping with GroupBy

GroupBy is where LINQ starts to feel like SQL. Each group is an IGrouping<TKey, TElement> — it has a Key and is itself enumerable.

var byCategory = SampleData.Products
    .GroupBy(p => p.Category)
    .Select(g => new
    {
        Category   = g.Key,
        Count      = g.Count(),
        TotalStock = g.Sum(p => p.Stock),
        AvgPrice   = g.Average(p => p.Price)
    });

foreach (var g in byCategory)
    Console.WriteLine($"{g.Category}: {g.Count} products, {g.TotalStock} in stock, avg {g.AvgPrice:C}");

Output:

Electronics: 3 products, 182 in stock, avg $551.16
Furniture: 2 products, 8 in stock, avg $319.00
Stationery: 2 products, 575 in stock, avg $7.99

5. Joining Collections with Join

LINQ Join in C# works like an SQL inner join: it matches elements from two sequences on a key and lets you project the combined result.

var orderDetails = SampleData.Orders
    .Join(SampleData.Products,
          order   => order.ProductId,   // outer key
          product => product.Id,        // inner key
          (order, product) => new
          {
              order.Id,
              product.Name,
              order.Quantity,
              LineTotal = order.Quantity * product.Price
          });

foreach (var od in orderDetails)
    Console.WriteLine($"Order {od.Id}: {od.Quantity} x {od.Name} = {od.LineTotal:C}");

The same thing in query syntax reads closer to SQL, which many teams prefer for multi-join queries:

var details = from o in SampleData.Orders
              join p in SampleData.Products on o.ProductId equals p.Id
              select new { o.Id, p.Name, LineTotal = o.Quantity * p.Price };

Left join with GroupJoin

Need products that have no orders too? Use GroupJoin plus DefaultIfEmpty:

var productsWithOrders = SampleData.Products
    .GroupJoin(SampleData.Orders,
               p => p.Id,
               o => o.ProductId,
               (p, orders) => new { p.Name, OrderCount = orders.Count() });

foreach (var x in productsWithOrders)
    Console.WriteLine($"{x.Name}: {x.OrderCount} orders");
// Desk Chair: 0 orders, Standing Desk: 0 orders ...

6. Aggregating with Sum, Average, Min, Max, and Count

Aggregates execute immediately and return a single value. Combine them with Where for targeted metrics.

decimal inventoryValue = SampleData.Products.Sum(p => p.Price * p.Stock);
decimal cheapest       = SampleData.Products.Min(p => p.Price);
Product priciest       = SampleData.Products.MaxBy(p => p.Price)!;   // .NET 6+
int outOfStock         = SampleData.Products.Count(p => p.Stock == 0);

Console.WriteLine($"Inventory value: {inventoryValue:C}");
Console.WriteLine($"Most expensive: {priciest.Name}");
Console.WriteLine($"Out of stock: {outOfStock}");

Pitfall: Average throws InvalidOperationException on an empty sequence. Guard with DefaultIfEmpty(0) or check Any() first.

7. Finding Single Elements: First, Single, and the OrDefault Variants

Picking the right one communicates intent and catches bugs early:

  • First() — at least one match expected; throws if none.
  • FirstOrDefault() — zero or more matches; returns null/default if none.
  • Single() — exactly one match expected; throws if zero or more than one.
  • SingleOrDefault() — zero or one match; throws if more than one.
var laptop  = SampleData.Products.Single(p => p.Name == "Laptop");
var missing = SampleData.Products.FirstOrDefault(p => p.Name == "Tablet"); // null

// .NET 6+ lets you supply a fallback:
var fallback = SampleData.Products.FirstOrDefault(p => p.Name == "Tablet",
                                                  new Product(0, "N/A", "", 0, 0));

Use Single when looking up by a unique key. If your "unique" key ever returns two rows, you want an exception, not silently wrong data.

8. Paging with Skip and Take

Every API list endpoint needs paging. Skip and Take make it trivial, and EF Core translates them to OFFSET/FETCH in SQL.

int pageSize = 3;
int totalPages = (int)Math.Ceiling(SampleData.Products.Count / (double)pageSize);

for (int page = 1; page <= totalPages; page++)
{
    var items = SampleData.Products
        .OrderBy(p => p.Id)               // ALWAYS order before paging
        .Skip((page - 1) * pageSize)
        .Take(pageSize);

    Console.WriteLine($"Page {page}: {string.Join(", ", items.Select(p => p.Name))}");
}

Why the OrderBy? Without a deterministic order, a database is free to return rows in any sequence, so page 2 may repeat items from page 1. Also check out Chunk(size) (.NET 6+) for splitting a sequence into fixed-size arrays in memory.

9. Set Operations: Distinct, Union, Intersect, Except

These are underused but incredibly handy for comparing lists — for example, syncing data between two systems.

var categories = SampleData.Products.Select(p => p.Category).Distinct();
// Electronics, Furniture, Stationery

var orderedProductIds = SampleData.Orders.Select(o => o.ProductId).Distinct();
var allProductIds     = SampleData.Products.Select(p => p.Id);

var neverOrdered = allProductIds.Except(orderedProductIds);      // 3, 4
var both         = allProductIds.Intersect(orderedProductIds);   // 1, 2, 5, 6, 7

// .NET 6+: DistinctBy for objects
var onePerCategory = SampleData.Products.DistinctBy(p => p.Category);

For custom classes, set operations rely on Equals/GetHashCode. Records give you value equality for free, which is one reason they pair so nicely with LINQ.

10. Flattening with SelectMany

SelectMany turns a sequence of sequences into one flat sequence. It's the operator that feels confusing until it clicks — then you use it everywhere.

var customers = new[]
{
    new { Name = "Alice", Tags = new[] { "vip", "newsletter" } },
    new { Name = "Bob",   Tags = new[] { "newsletter" } },
    new { Name = "Cara",  Tags = Array.Empty<string>() },
};

// All tags, flattened and deduplicated
var allTags = customers.SelectMany(c => c.Tags).Distinct();
// vip, newsletter

// Keep the parent alongside each child
var pairs = customers.SelectMany(c => c.Tags, (c, tag) => $"{c.Name} -> {tag}");
// Alice -> vip, Alice -> newsletter, Bob -> newsletter

Notice Cara disappears from the output entirely — an empty inner sequence contributes nothing. If that's not what you want, use DefaultIfEmpty() on the inner sequence.

C# LINQ Best Practices and Common Pitfalls

Understand deferred execution

Most LINQ operators (Where, Select, OrderBy, GroupBy, Join) are lazy. The query runs each time you enumerate it. This code hits the source twice:

var expensive = SampleData.Products.Where(p => p.Price > 100);
Console.WriteLine(expensive.Count());   // enumerates once
foreach (var p in expensive) { }        // enumerates again

For in-memory lists that's cheap; against a database it's two round trips. Materialize with ToList() or ToArray() when you'll iterate more than once. Modern analyzers (CA1851) flag "possible multiple enumeration" for exactly this reason.

Prefer specific overloads over chains

Count(predicate) beats Where(predicate).Count(); Any(predicate) beats Where(predicate).Any(); and Any() beats Count() > 0, because Any stops at the first match while Count walks the whole sequence.

Know when LINQ is translated vs. executed in memory

With EF Core, everything before a ToList()/AsEnumerable() becomes SQL. Calling a C# method the provider can't translate inside Where throws at runtime. Put the filter first, pull to memory, then apply custom logic.

Don't over-LINQ hot paths

LINQ allocates iterators and delegates. In tight loops running millions of times — game loops, parsers, high-frequency services — a plain for loop can be several times faster. Measure with BenchmarkDotNet before rewriting, but be aware of the trade-off.

Method syntax vs. query syntax

Both compile to the same calls. Method syntax (.Where().Select()) supports every operator; query syntax (from ... select) is more readable for joins and let clauses. Pick one style per codebase and stay consistent.

Conclusion: Key Takeaways from This C# LINQ Tutorial

LINQ in C# is the fastest path from "I have a collection" to "I have the answer." The 10 examples in this C# LINQ tutorial cover more than 90% of what you'll write day to day:

  • Where / Select — filter and shape data; both are lazy and compose freely.
  • OrderBy / ThenBy — stable, multi-key sorting; always order before paging.
  • GroupBy / Join / GroupJoin — SQL-style grouping, inner joins, and left joins.
  • Sum / Average / MaxBy / Count — immediate aggregates; guard empty sequences.
  • First / Single + OrDefault — express your expectations and fail fast.
  • Skip / Take / Chunk — paging done right.
  • Distinct / Except / Intersect / DistinctBy — set logic for comparing lists.
  • SelectMany — flatten nested collections.
  • Deferred execution — materialize once when you enumerate more than once.

Master these, internalize the deferred-execution model, and you'll write C# that's shorter, clearer, and easier to review. Next, try rewriting a nested foreach in your own project using GroupBy or SelectMany — it's the best way to make these operators second nature.

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

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

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...