Skip to main content

    Lesson 11 • Expert Track

    LINQ

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

    What You'll Learn

    • Filter a collection with Where and reshape it with Select
    • Sort with OrderBy / OrderByDescending and group with GroupBy
    • Reduce a sequence to one value: Sum, Count, Average, Any, All
    • Pick single items safely with First vs FirstOrDefault (and Single)
    • Tell deferred (lazy) execution from immediate (ToList) — and avoid the traps
    • Choose between method syntax and query syntax for the same result

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

    Worked example: Where, Select, chaining & aggregates

    Read every comment, run it, and check each output matches.

    Try it Yourself »
    C#
    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.Joi
    ...

    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.

    🎯 Your turn: finish the Where filter

    Complete the lambda so only scores above 5 survive.

    Try it Yourself »
    C#
    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 
    ...

    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.

    🎯 Your turn: sort, then project

    Fill in the OrderBy key and the Select value, then check the output order.

    Try it Yourself »
    C#
    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 "
    ...

    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.

    Worked example: querying a list of students

    Filter by GPA, sort, project, and answer Any/All questions.

    Try it Yourself »
    C#
    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 },
              
    ...

    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.

    Worked example: lazy queries vs ToList snapshots

    See a deferred query pick up a late change, and ToList freeze the result.

    Try it Yourself »
    C#
    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
    ...

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

    Worked example: both syntaxes + GroupBy

    Compare query and method syntax side by side, then group by category.

    Try it Yourself »
    C#
    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()
        {
            var products = new List<Product>
            {
                new Product { Name = "Laptop",     Category = "Electronics", Price = 999m },
                new Product { Name = "Phone",      Category = "Electronics", Price = 699m },
                new Produc
    ...

    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.

    Worked example: a paid-orders report

    Filter, sort, project, and summarise a list of orders in one go.

    Try it Yourself »
    C#
    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()
        {
            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,  Pa
    ...

    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

    • 💡 LINQ is lazy: nothing runs until you enumerate. If you'll iterate a query more than once — or the source might change — call .ToList() once and reuse it.
    • 💡 Prefer FirstOrDefault over First whenever "no match" is a real possibility; then check the result for null instead of catching an exception.
    • 💡 Chain reads like a sentence: .Where().OrderBy().Select() — filter, then sort, then reshape. Keep that order and your queries stay readable.
    • 💡 Use the right aggregate: Count(x => ...), Sum(x => ...), and Average(x => ...) take a selector, so you rarely need a manual loop with a running total.
    • 💡 Method syntax wins in production — it's more concise and composable — but use query syntax when a join or group genuinely reads more clearly that way.

    Common Errors (and the fix)

    • "CS1061: 'List<int>' does not contain a definition for 'Where'": you forgot using System.Linq; at the top. The LINQ methods are extension methods that only appear once that namespace is imported.
    • "System.InvalidOperationException: Sequence contains no elements": you called First() (or Single(), Average()) on an empty result. Use FirstOrDefault() and check for null, or test with .Any() first.
    • "Sequence contains more than one matching element": Single(...) found two or more matches. Use First(...) if you only want the first, or fix the data so the value really is unique.
    • Deferred-execution surprise: your query "changed" after you defined it. It didn't — it just ran later and saw the updated source. Add .ToList() to snapshot the result at the point you define it.
    • "System.NullReferenceException" inside a Select: a lambda touched a property on a null item, e.g. .Select(s => s.Name.ToUpper()) when an s is null. Filter first (.Where(s => s != null)) or use the null-conditional s?.Name.

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

    🎯 Mini-Challenge: chain Where + OrderBy + Select

    Filter to over £50, sort cheapest first, project to a label, and print.

    Try it Yourself »
    C#
    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   
    ...

    🎉 Lesson Complete

    • Where filters, Select transforms (projects) — the two LINQ workhorses
    • OrderBy/OrderByDescending sort; GroupBy buckets items by a key
    • Sum, Count, Average, Any, All reduce a sequence to one value
    • FirstOrDefault is safe on empty; First/Single throw — choose deliberately
    • ✅ Queries are deferred (lazy) until enumerated; ToList() runs & snapshots them
    • ✅ Method syntax and query syntax compile to the same thing; both work on IEnumerable<T>
    • Next lesson: Async & Await — writing non-blocking code that doesn't freeze your app

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service