LINQ

Reviewed & published by Brayan K

By the end of this lesson you'll be able to query any collection in C# the way you'd ask a question in plain English — filtering, sorting, reshaping, and summarising data with short, readable chains instead of hand-written loops. This is the single most-used feature in everyday C#.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

LINQ is like having a personal assistant for your data. Without it, finding the right records means opening a filing cabinet and flicking through every folder by hand (a for loop). With LINQ you just say: "give me all invoices over £500, newest first, and only the customer names." The assistant does the searching (Where), the sorting (OrderBy), and the tidying-up (Select) — and hands you exactly what you asked for, in one clean sentence. LINQ stands for Language Integrated Query: a query language baked right into C#.

📊 The LINQ Operators You'll Use Daily

OperatorWhat it doesExampleReturns
WhereKeep matching items.Where(n => n > 5)a sequence
SelectTransform each item.Select(n => n * 2)a sequence
OrderBySort ascending.OrderBy(p => p.Price)a sequence
GroupByBucket by a key.GroupBy(p => p.Cat)groups
First / FirstOrDefaultGet the first match.First(x => x.Ok)one item
Any / AllTest the sequence.Any(x => x < 0)bool
Count / Sum / AverageReduce to one number.Sum(o => o.Total)a number
ToListRun now & snapshot.ToList()List<T>

Everything in LINQ works on IEnumerable<T> — the common interface every list, array, and most collections implement. That's why one set of operators works on all of them.

Running C# locally: install the .NET SDK or use dotnetfiddle.net. Remember using System.Linq; at the top of every file that queries.

1. Filtering & Transforming — Where and Select

The two workhorses of LINQ are Where and Select. Where filters: you give it a test (a lambda that returns true or false) and it keeps only the items that pass. Select transforms: it runs a lambda on each item and gives you back the results — a process called projection. A lambda like n => n > 5 is just a tiny inline function: read => as "goes to". Read this worked example, run it, then you'll write your own.

using System;
using System.Linq;                 // 👈 this 'using' is what makes LINQ work
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

        // Where — KEEP only the items where the test is true.
        // 'n' is each element in turn; '=>' reads as "goes to".
        var evens = numbers.Where(n => n % 2 == 0);   // 2, 4, 6, 8, 10
        Console.WriteLine("Evens:   " + string.Join(", ", evens));

        // Select — TRANSFORM each item into something new (here, doubled).
        var doubled = numbers.Select(n => n * 2);     // 2, 4, 6, ... 20
        Console.WriteLine("Doubled: " + string.Join(", ", doubled));

        // Chaining — read it top to bottom like a sentence:
        // "take numbers, keep the ones > 5, then square each one".
        var result = numbers
            .Where(n => n > 5)        // 6, 7, 8, 9, 10
            .Select(n => n * n);      // 36, 49, 64, 81, 100
        Console.WriteLine("Squares of >5: " + string.Join(", ", result));

        // Aggregate methods reduce a whole sequence to ONE value.
        Console.WriteLine($"Sum:   {numbers.Sum()}");                  // 55
        Console.WriteLine($"Count of evens: {numbers.Count(n => n % 2 == 0)}"); // 5
    }
}

// ✅ Expected output:
//    Evens:   2, 4, 6, 8, 10
//    Doubled: 2, 4, 6, 8, 10, 12, 14, 16, 18, 20
//    Squares of >5: 36, 49, 64, 81, 100
//    Sum:   55
//    Count of evens: 5

Your turn. The program below is almost complete — fill in the blank in the Where lambda so it keeps scores greater than 5, then run it and check the output.

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

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — fill in the blanks marked with ___, then run it.

        List<int> scores = new List<int> { 3, 8, 1, 10, 5, 7, 2, 9 };

        // 1) Keep ONLY the scores greater than 5.
        //    Where(...) takes a lambda that returns true for items to KEEP.
        var passed = scores.Where(n => ___);   // 👉 the test:  n > 5

        // These lines already work once your filter is correct:
        Console.WriteLine("Passed: " + string.Join(", ", passed));
        Console.WriteLine($"How many passed? {passed.Count()}");

        // ✅ Expected output:
        //    Passed: 8, 10, 7, 9
        //    How many passed? 4
    }
}

2. Sorting & Projecting — OrderBy + Select

OrderBy sorts a sequence using the key its lambda returns — OrderBy(p => p.Price) sorts by price, smallest first; OrderByDescending reverses it. You'll almost always pair sorting with a Select projection to reshape each item into the form you want to show — a label, a summary, or a new object. Now you try: sort the prices, then project each into a tidy label.

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

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — fill in the two blanks, then run it.

        List<int> prices = new List<int> { 30, 10, 50, 20, 40 };

        // 1) Sort the prices from smallest to largest.
        //    OrderBy(...) sorts using the key the lambda returns.
        var sorted = prices.OrderBy(p => ___);   // 👉 sort by the value itself:  p

        // 2) Turn each price into a label like "Price: £30".
        //    Select(...) transforms each item into a new shape.
        var labels = sorted.Select(p => $"Price: £{___}");  // 👉 the value:  p

        foreach (var label in labels)
            Console.WriteLine(label);

        // ✅ Expected output:
        //    Price: £10
        //    Price: £20
        //    Price: £30
        //    Price: £40
        //    Price: £50
    }
}

3. Querying Collections of Objects

LINQ truly shines on lists of objects. You filter by a property (s => s.GPA > 3.6), sort by another, then project into exactly the shape you need. The single-item operators earn their keep here too: First grabs the first match (and throws if there isn't one), while Any and All answer yes/no questions about the whole sequence. Aggregates like Average(s => s.GPA) take a selector so they know which number to crunch.

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

class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
    public double GPA { get; set; }
}

class Program
{
    static void Main()
    {
        var students = new List<Student>
        {
            new Student { Name = "Alice",   Age = 20, GPA = 3.8 },
            new Student { Name = "Bob",     Age = 22, GPA = 3.2 },
            new Student { Name = "Charlie", Age = 19, GPA = 3.9 },
            new Student { Name = "Diana",   Age = 21, GPA = 3.5 },
            new Student { Name = "Eve",     Age = 20, GPA = 3.7 }
        };

        // Filter by a property, sort, then PROJECT into a printable string.
        var honours = students
            .Where(s => s.GPA > 3.6)                 // Alice, Charlie, Eve
            .OrderByDescending(s => s.GPA)           // highest GPA first
            .Select(s => $"{s.Name} (GPA: {s.GPA})");

        Console.WriteLine("Honour students:");
        foreach (var h in honours)
            Console.WriteLine($"  {h}");
        // Charlie (GPA: 3.9), Alice (GPA: 3.8), Eve (GPA: 3.7)

        // First — the first match (throws if there is NO match).
        var youngest = students.OrderBy(s => s.Age).First();
        Console.WriteLine($"Youngest: {youngest.Name} (age {youngest.Age})"); // Charlie, 19

        // Aggregates with a selector pull one number from the objects.
        Console.WriteLine($"Average GPA: {students.Average(s => s.GPA):F2}"); // 3.62

        // Any / All return a single bool.
        Console.WriteLine($"Any under 18? {students.Any(s => s.Age < 18)}");  // False
        Console.WriteLine($"All above 3.0? {students.All(s => s.GPA > 3.0)}");// True
    }
}

// ✅ Expected output:
//    Honour students:
//      Charlie (GPA: 3.9)
//      Alice (GPA: 3.8)
//      Eve (GPA: 3.7)
//    Youngest: Charlie (age 19)
//    Average GPA: 3.62
//    Any under 18? False
//    All above 3.0? True

4. Deferred vs Immediate Execution

This is the LINQ behaviour that surprises everyone once. A LINQ query is deferred (lazy): defining it doesn't run anything — it just stores the recipe. The work happens later, the moment you enumerate it (a foreach, string.Join, Count(), etc.). So if the source list changes between defining and running the query, the query sees the change. Calling ToList() (or ToArray()) runs it immediately and snapshots the result, freezing it. Watch both happen.

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

class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 3 };

        // A LINQ query is just a RECIPE — it does NOT run yet.
        var query = numbers.Where(n => n > 1);   // nothing has happened

        numbers.Add(4);   // we change the source AFTER defining the query

        // The query runs NOW, when we enumerate it — so it 'sees' the 4.
        Console.WriteLine(string.Join(", ", query));   // 2, 3, 4  (surprised?)

        // ToList() runs the query IMMEDIATELY and freezes the result.
        var snapshot = numbers.Where(n => n > 1).ToList();  // [2, 3, 4] right now
        numbers.Add(5);                                      // changing source again...
        Console.WriteLine(string.Join(", ", snapshot));      // 2, 3, 4  (frozen!)
    }
}

// ✅ Expected output:
//    2, 3, 4
//    2, 3, 4

🔎 Deep Dive: First, FirstOrDefault and Single

These four operators all return one item, but they fail very differently — and choosing the wrong one is a classic source of crashes.

list.First();                  // first item — THROWS if the list is empty
list.First(x => x.Ok);         // first match — THROWS if nothing matches
list.FirstOrDefault();         // first item, or default (null / 0) if empty — never throws
list.Single(x => x.Id == 5);   // the ONE match — THROWS if 0 OR more than 1 match
list.SingleOrDefault(...);     // the one match, or default — THROWS only if MORE than one

Rule of thumb: use FirstOrDefault when "no match" is normal and you'll check for null; use First only when you're certain a match exists; reach for Single when a value must be unique (like a lookup by primary key) and you want it to blow up loudly if it isn't.

5. Query Syntax vs Method Syntax

C# gives you two ways to write the same query. Method syntax uses the extension methods and lambdas you've used so far (.Where(...).OrderBy(...).Select(...)) — it's the most common in production code and composes freely. Query syntax (from ... where ... orderby ... select) reads like SQL and can be clearer for joins and grouping. They compile to the same thing, so pick whichever is more readable. This example shows both, plus GroupBy to bucket items by a key.

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

class Product
{
    public string Name { get; set; }
    public string Category { get; set; }
    public decimal Price { get; set; }
}

class Program
{
    static void Main()
    {
        // Currency formatting follows the thread's culture: {value:C} prints
        // £ in London, $ in Boston and € in Paris. Pin it when the output has
        // to be the same everywhere — as it does on a page that shows you the
        // result.
        CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");

        var products = new List<Product>
        {
            new Product { Name = "Laptop",     Category = "Electronics", Price = 999m },
            new Product { Name = "Phone",      Category = "Electronics", Price = 699m },
            new Product { Name = "Desk",       Category = "Furniture",   Price = 299m },
            new Product { Name = "Chair",      Category = "Furniture",   Price = 199m },
            new Product { Name = "Headphones", Category = "Electronics", Price = 149m }
        };

        // METHOD syntax — extension methods + lambdas. Most common in real code.
        var expensiveMethod = products
            .Where(p => p.Price > 200)
            .OrderByDescending(p => p.Price)
            .Select(p => $"{p.Name}: {p.Price:C}");

        Console.WriteLine("Over £200 (method syntax):");
        foreach (var line in expensiveMethod)
            Console.WriteLine($"  {line}");

        // QUERY syntax — reads like SQL. Identical result; pick whichever is clearer.
        var expensiveQuery = from p in products
                             where p.Price > 200
                             orderby p.Price descending
                             select $"{p.Name}: {p.Price:C}";

        Console.WriteLine("Over £200 (query syntax):");
        foreach (var line in expensiveQuery)
            Console.WriteLine($"  {line}");

        // GroupBy — bucket items by a key, then summarise each group.
        var byCategory = products.GroupBy(p => p.Category);
        Console.WriteLine("Items per category:");
        foreach (var group in byCategory)
            Console.WriteLine($"  {group.Key}: {group.Count()} item(s)");
        // Electronics: 3 item(s) / Furniture: 2 item(s)
    }
}

// ✅ Expected output:
//    Over £200 (method syntax):
//      Laptop: £999.00
//      Phone: £699.00
//      Desk: £299.00
//    Over £200 (query syntax):
//      Laptop: £999.00
//      Phone: £699.00
//      Desk: £299.00
//    Items per category:
//      Electronics: 3 item(s)
//      Furniture: 2 item(s)

Putting It Together: an Orders Report

Here's a small but real program that uses everything from this lesson at once — a Where filter with two conditions, an OrderByDescending sort, a Select projection into a formatted line, and a couple of one-line aggregates straight off the source list. You understand every part now.

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

class Order
{
    public string Customer { get; set; }
    public decimal Total { get; set; }
    public bool Paid { get; set; }
}

class Program
{
    static void Main()
    {
        // Currency formatting follows the thread's culture: {value:C} prints
        // £ in London, $ in Boston and € in Paris. Pin it when the output has
        // to be the same everywhere — as it does on a page that shows you the
        // result.
        CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");

        var orders = new List<Order>
        {
            new Order { Customer = "Ada",   Total = 120m, Paid = true  },
            new Order { Customer = "Brian", Total = 540m, Paid = true  },
            new Order { Customer = "Cara",  Total = 75m,  Paid = false },
            new Order { Customer = "Dev",   Total = 300m, Paid = true  },
            new Order { Customer = "Erin",  Total = 90m,  Paid = false }
        };

        // A real report: paid orders over £100, biggest first, as receipts.
        var report = orders
            .Where(o => o.Paid && o.Total > 100)     // keep paid & sizeable
            .OrderByDescending(o => o.Total)          // biggest spenders first
            .Select(o => $"{o.Customer,-6} {o.Total:C}");

        Console.WriteLine("Top paid orders:");
        foreach (var line in report)
            Console.WriteLine($"  {line}");

        // One-line summaries straight off the source list.
        Console.WriteLine($"Paid revenue: {orders.Where(o => o.Paid).Sum(o => o.Total):C}");
        Console.WriteLine($"Unpaid count: {orders.Count(o => !o.Paid)}");
    }
}

// ✅ Expected output:
//    Top paid orders:
//      Brian  £540.00
//      Dev    £300.00
//      Ada    £120.00
//    Paid revenue: £960.00
//    Unpaid count: 2

Notice the report and the two summary lines each enumerate the list independently — that's deferred execution at work. For a handful of orders that's fine; for a huge or expensive source, call ToList() once and reuse the result.

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeResult
Filternums.Where(n => n > 5)matching items
Transformnums.Select(n => n * 2)new items
Sortitems.OrderBy(x => x.Key)ascending
Groupitems.GroupBy(x => x.Cat)groups by key
First matchitems.FirstOrDefault(p)item or null
Any / Allitems.Any(x => x.Ok)true / false
Sum / Countitems.Sum(x => x.Total)a number
Run nowquery.ToList()snapshot list

Frequently Asked Questions

Q: My query "re-ran" and gave different results — is that a bug?

No, that's deferred execution. A LINQ query is a recipe that runs each time you enumerate it, so it always reflects the source at that moment. If you want a fixed result, call .ToList() to run it once and snapshot it.

Q: When should I use First versus FirstOrDefault?

Use FirstOrDefault when "no match" is a normal outcome — it returns null (or 0 for numbers) instead of throwing. Use First only when you're certain a match exists and an empty result genuinely is a bug.

Q: Is query syntax or method syntax "better"?

Neither — they compile to the same code. Method syntax is more common and composes more freely, so most teams default to it. Query syntax can read more clearly for joins and grouping. Pick whichever is easier to read for the query at hand.

Q: What is IEnumerable<T> and why do I keep seeing it?

It's the interface that means "a sequence you can iterate one item at a time." Lists, arrays, and most collections implement it, and LINQ operators both take and return it — which is exactly why the same Where/Select work on all of them, and why a chain stays lazy until you enumerate.

Mini-Challenge: Premium Products

No blanks this time — just a brief and an outline to keep you on track. Starting from the list of products, chain Where + OrderBy + Select to build a list of premium products (over £50), cheapest first, formatted as "Name — £Price", then print each line. Run it and check your output against the expected lines in the comments.

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

class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

class Program
{
    static void Main()
    {
        var products = new List<Product>
        {
            new Product { Name = "Mouse",    Price = 25m  },
            new Product { Name = "Keyboard", Price = 75m  },
            new Product { Name = "Monitor",  Price = 250m },
            new Product { Name = "Cable",    Price = 8m   },
            new Product { Name = "Webcam",   Price = 60m  }
        };

        // 🎯 MINI-CHALLENGE: build a "premium products" list
        // 1. Where:    keep only products priced over £50.
        // 2. OrderBy:  sort by Price, cheapest first.
        // 3. Select:   turn each into the string  "Name — £Price"  (use :C or £{Price}).
        // 4. foreach over the result and print each line.
        //
        // ✅ Expected output:
        //    Webcam — £60.00
        //    Keyboard — £75.00
        //    Monitor — £250.00

        // your code here
    }
}

🎉 Lesson Complete

Practice quiz

What does the LINQ Where operator do?

  • Transforms each item into a new shape
  • Keeps only the items for which the test returns true
  • Sorts the sequence
  • Reduces the sequence to one value

Answer: Keeps only the items for which the test returns true. Where filters: it keeps only the items whose lambda returns true. Select is the one that transforms.

What does the Select operator do?

  • Filters out non-matching items
  • Groups items by a key
  • Transforms (projects) each item into something new
  • Counts the items

Answer: Transforms (projects) each item into something new. Select runs a lambda on each item and returns the results — a process called projection.

What does 'deferred execution' mean for a LINQ query?

  • The query runs once and caches forever
  • Defining the query runs nothing; the work happens when you enumerate it
  • The query runs on a background thread
  • The query is compiled to SQL

Answer: Defining the query runs nothing; the work happens when you enumerate it. A LINQ query is a recipe. Defining it runs nothing; the work happens the moment you enumerate it (foreach, Count(), etc.).

What does calling ToList() on a query do?

  • Keeps it deferred and lazy
  • Runs it immediately and snapshots the result
  • Sorts the result
  • Removes duplicates

Answer: Runs it immediately and snapshots the result. ToList() (or ToArray()) forces immediate execution and freezes the result into a concrete list, so later source changes don't affect it.

What happens when you call First() on an empty sequence?

  • It returns null
  • It returns 0
  • It throws InvalidOperationException
  • It returns an empty sequence

Answer: It throws InvalidOperationException. First() throws 'Sequence contains no elements' on an empty sequence. Use FirstOrDefault() when no match is a normal outcome.

How does FirstOrDefault() behave when there is no match?

  • Throws an exception
  • Returns the default value (null for reference types, 0 for numbers)
  • Returns the last item
  • Returns the whole sequence

Answer: Returns the default value (null for reference types, 0 for numbers). FirstOrDefault() never throws on an empty result; it returns the type's default (null or 0), which you then check for.

When does Single(predicate) throw?

  • Only when the sequence is empty
  • Never
  • When there are zero matches OR more than one match
  • Only when there are more than two matches

Answer: When there are zero matches OR more than one match. Single requires exactly one match; it throws if there are zero OR more than one. Use it when a value must be unique.

What does GroupBy return?

  • A single number
  • Groups, each with a Key and the items in that bucket
  • A sorted sequence
  • A boolean

Answer: Groups, each with a Key and the items in that bucket. GroupBy buckets items by a key; each group is an IGrouping with a .Key and is itself a sequence you can Count or Sum.

Which interface do LINQ operators both take and return, letting the same Where/Select work on lists and arrays?

  • IComparable<T>
  • IDisposable
  • IEnumerable<T>
  • ICollection

Answer: IEnumerable<T>. LINQ operators work on IEnumerable<T>, which lists, arrays, and most collections implement — that's why one operator set works on all of them.

Why might a query appear to 'change' after you define it?

  • It is a compiler bug
  • Because of deferred execution — it runs later and reflects the source at that moment
  • Because Where mutates the source
  • Because Select caches stale data

Answer: Because of deferred execution — it runs later and reflects the source at that moment. Deferred execution means the query runs each time you enumerate it, reflecting the source as it is then. Call ToList() to snapshot it.

Continue this course