Advanced LINQ Queries & Optimization

Reviewed & published by Brayan K

By the end of this lesson you'll be able to group, flatten, join, and reduce real-world data with LINQ — and write queries that stay fast and predictable in production by mastering deferred execution and materialisation.

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

Think of a busy mailroom. GroupBy is sorting the post into pigeonholes by recipient, then counting each pile. SelectMany is tipping every pigeonhole out onto one big table — flattening many piles into a single stream. Join is matching each letter to its recipient's address card by a shared name. And deferred execution is the difference between writing the sorting instructions on a clipboard (the query) and actually walking the room to do it (enumerating) — nothing moves until someone reads the clipboard and acts.

📊 Advanced LINQ Methods Reference

MethodWhat it doesReturns
GroupByBucket items by a keyIEnumerable<IGrouping>
SelectManyFlatten nested sequencesone flat sequence
JoinMatch two sources on a keycombined sequence
AggregateFold to one value (reduce)a single value
Skip / TakePage through a sequencea sub-sequence
ToList / ToArrayRun now & snapshotList<T> / T[]
ToDictionaryBuild O(1) key lookupDictionary<K,V>
ToLookupMaterialised one-to-many groupingILookup<K,V>

Every one of these still works on IEnumerable<T>. The To... family is special: those are the methods that force execution and turn a lazy recipe into concrete data.

Running C# locally: install the .NET SDK or use dotnetfiddle.net. Every example here uses an in-memory List<T>, so it runs unchanged in a console app. Keep using System.Linq; at the top.

1. GroupBy — Bucket & Summarise

GroupBy splits a collection into groups using the key its lambda returns. The clever part: each group is an IGrouping<K, T> — it carries a .Key and it is itself a sequence of the items in that bucket. That means you can call Count(), Sum(...), or Average(...) straight on a group. Pair it with Select to turn each group into a tidy summary object. Read this worked example, run it, then you'll write your own.

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

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

class Program
{
    static void Main()
    {
        var orders = new List<Order>
        {
            new Order { Customer = "Alice", Product = "Laptop",   Price = 999, Category = "Electronics" },
            new Order { Customer = "Bob",   Product = "Keyboard", Price = 79,  Category = "Electronics" },
            new Order { Customer = "Alice", Product = "Book",     Price = 25,  Category = "Books" },
            new Order { Customer = "Carol", Product = "Mouse",    Price = 49,  Category = "Electronics" },
            new Order { Customer = "Bob",   Product = "Novel",    Price = 15,  Category = "Books" },
            new Order { Customer = "Alice", Product = "Monitor",  Price = 450, Category = "Electronics" },
        };

        // GroupBy(keySelector) buckets items by a key.
        // Each 'group' is an IGrouping: it has a .Key AND is itself a sequence
        // of all the items in that bucket — so you can Count/Sum over it.
        var byCustomer = orders.GroupBy(o => o.Customer);

        Console.WriteLine("=== Orders by Customer ===");
        foreach (var group in byCustomer)
        {
            // group.Key = the customer; group is the items for that customer.
            decimal spent = group.Sum(o => o.Price);   // total per customer
            Console.WriteLine($"{group.Key}: {group.Count()} orders, Total £{spent}");
            foreach (var order in group)
                Console.WriteLine($"  - {order.Product}: £{order.Price}");
        }
        // Alice: 3 orders, Total £1474
        // Bob:   2 orders, Total £94
        // Carol: 1 orders, Total £49

        // GroupBy + Select — turn each group into a tidy summary object.
        var categorySummary = orders
            .GroupBy(o => o.Category)
            .Select(g => new
            {
                Category = g.Key,
                Count    = g.Count(),
                Total    = g.Sum(o => o.Price),
                Average  = g.Average(o => o.Price)
            });

        Console.WriteLine("\n=== Category Summary ===");
        foreach (var cat in categorySummary)
            Console.WriteLine($"{cat.Category}: {cat.Count} items, Total £{cat.Total}, Avg £{cat.Average:F2}");
        // Electronics: 4 items, Total £1577, Avg £394.25
        // Books:       2 items, Total £40,   Avg £20.00
    }
}

// ✅ Expected output:
//    === Orders by Customer ===
//    Alice: 3 orders, Total £1474
//      - Laptop: £999
//      - Book: £25
//      - Monitor: £450
//    Bob: 2 orders, Total £94
//      - Keyboard: £79
//      - Novel: £15
//    Carol: 1 orders, Total £49
//      - Mouse: £49
//
//    === Category Summary ===
//    Electronics: 4 items, Total £1577, Avg £394.25
//    Books: 2 items, Total £40, Avg £20.00

Your turn. The program below groups sales by region — fill in the three blanks so it counts the sales and sums the amount per region, then run it and check the output.

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

class Sale
{
    public string Region { get; set; }
    public decimal Amount { get; set; }
}

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

        var sales = new List<Sale>
        {
            new Sale { Region = "North", Amount = 100 },
            new Sale { Region = "South", Amount = 250 },
            new Sale { Region = "North", Amount = 75  },
            new Sale { Region = "East",  Amount = 300 },
            new Sale { Region = "South", Amount = 50  },
        };

        // 1) Group the sales by Region.
        //    GroupBy(...) takes a lambda that returns the KEY to bucket by.
        var byRegion = sales.GroupBy(s => ___);     // 👉 the key:  s.Region

        foreach (var group in byRegion)
        {
            // 2) Count how many sales are in this group.
            int count = group.___;                  // 👉 Count()

            // 3) Sum the Amount of every sale in this group.
            decimal total = group.Sum(s => ___);    // 👉 the value:  s.Amount

            Console.WriteLine($"{group.Key}: {count} sales, £{total}");
        }

        // ✅ Expected output:
        //    North: 2 sales, £175
        //    South: 2 sales, £300
        //    East: 1 sales, £300
    }
}

2. SelectMany — Flatten Nested Data

Select maps each item to something new — but if each item is itself a list, you end up with a list of lists. SelectMany solves this by flattening: it runs your lambda, then splices each returned sequence into one continuous stream. It's the go-to for departments→employees, posts→tags, or orders→line-items. A second overload takes a result selector so you can pair each child with its parent in one pass.

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

class Department
{
    public string Name { get; set; }
    public List<string> Employees { get; set; }
}

class Program
{
    static void Main()
    {
        var departments = new List<Department>
        {
            new Department { Name = "Engineering", Employees = new List<string> { "Alice", "Bob", "Carol" } },
            new Department { Name = "Marketing",   Employees = new List<string> { "Dave", "Eve" } },
            new Department { Name = "Design",      Employees = new List<string> { "Frank", "Grace", "Heidi" } },
        };

        // Select would give you a sequence OF LISTS (List<List<string>>).
        // SelectMany FLATTENS those nested lists into ONE flat sequence.
        var allEmployees = departments.SelectMany(d => d.Employees);
        Console.WriteLine($"All {allEmployees.Count()} employees: {string.Join(", ", allEmployees)}");
        // All 8 employees: Alice, Bob, Carol, Dave, Eve, Frank, Grace, Heidi

        // SelectMany with a result selector — pair each child with its parent.
        var withDept = departments.SelectMany(
            d => d.Employees,                                  // the inner sequence
            (dept, emp) => new { Department = dept.Name, Employee = emp } // combine
        );

        Console.WriteLine("\n=== Staff with Department ===");
        foreach (var item in withDept)
            Console.WriteLine($"  {item.Employee} — {item.Department}");

        // Flatten + Distinct: collect every unique tag across all posts.
        var posts = new[]
        {
            new { Title = "C# Tips",    Tags = new[] { "csharp", "dotnet", "tips" } },
            new { Title = "LINQ Guide", Tags = new[] { "csharp", "linq" } },
        };
        var allTags = posts.SelectMany(p => p.Tags).Distinct().OrderBy(t => t);
        Console.WriteLine($"\nUnique tags: {string.Join(", ", allTags)}");
        // Unique tags: csharp, dotnet, linq, tips
    }
}

// ✅ Expected output:
//    All 8 employees: Alice, Bob, Carol, Dave, Eve, Frank, Grace, Heidi
//
//    === Staff with Department ===
//      Alice — Engineering
//      Bob — Engineering
//      Carol — Engineering
//      Dave — Marketing
//      Eve — Marketing
//      Frank — Design
//      Grace — Design
//      Heidi — Design
//
//    Unique tags: csharp, dotnet, linq, tips

Now you try. Each shopping basket holds its own list of prices. Flatten them all into one sequence, then chain an aggregate to total them. Fill in the two blanks:

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

class Basket
{
    public string Owner { get; set; }
    public List<int> Prices { get; set; }
}

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

        var baskets = new List<Basket>
        {
            new Basket { Owner = "Ada",  Prices = new List<int> { 10, 20, 5 } },
            new Basket { Owner = "Ben",  Prices = new List<int> { 40, 60 } },
            new Basket { Owner = "Cleo", Prices = new List<int> { 15 } },
        };

        // 1) FLATTEN every basket's Prices into one sequence of ints.
        //    SelectMany(...) returns the inner list to splice in.
        var allPrices = baskets.SelectMany(b => ___);   // 👉 the inner list:  b.Prices

        // 2) Add up every price across all baskets.
        //    Sum() with no selector totals a sequence of numbers.
        int grandTotal = allPrices.___;                 // 👉 Sum()

        Console.WriteLine($"Items: {allPrices.Count()}");
        Console.WriteLine($"Grand total: £{grandTotal}");
        Console.WriteLine($"Most expensive: £{allPrices.Max()}");

        // ✅ Expected output:
        //    Items: 6
        //    Grand total: £165
        //    Most expensive: £60
    }
}

3. Join — Combine Two Sources

Real data is rarely in one list. Join matches rows from two sequences on a shared key — exactly like an inner JOIN in SQL. You give it the second source, a key selector for each side, and a result selector that combines the matched pair. Items with no match on the other side are simply dropped (that's an inner join). It's how you stitch customers to their purchases, or orders to their products.

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

class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

class Purchase
{
    public int CustomerId { get; set; }
    public string Product { get; set; }
    public decimal Amount { get; set; }
}

class Program
{
    static void Main()
    {
        var customers = new List<Customer>
        {
            new Customer { Id = 1, Name = "Alice" },
            new Customer { Id = 2, Name = "Bob" },
            new Customer { Id = 3, Name = "Carol" },
        };

        var purchases = new List<Purchase>
        {
            new Purchase { CustomerId = 1, Product = "Laptop", Amount = 999 },
            new Purchase { CustomerId = 2, Product = "Mouse",  Amount = 49  },
            new Purchase { CustomerId = 1, Product = "Book",   Amount = 25  },
            new Purchase { CustomerId = 3, Product = "Pen",    Amount = 3   },
        };

        // Join matches rows from TWO sources by a shared key — like a SQL JOIN.
        // (customers.Id  ==  purchases.CustomerId)
        var report = customers.Join(
            purchases,                 // the second source
            c => c.Id,                 // key from the first source
            p => p.CustomerId,         // key from the second source
            (c, p) => new { c.Name, p.Product, p.Amount }  // combine the pair
        );

        Console.WriteLine("=== Purchases by Customer ===");
        foreach (var row in report)
            Console.WriteLine($"  {row.Name} bought {row.Product} for £{row.Amount}");
        // Alice bought Laptop for £999
        // Alice bought Book   for £25
        // Bob   bought Mouse  for £49
        // Carol bought Pen    for £3
    }
}

// ✅ Expected output:
//    === Purchases by Customer ===
//      Alice bought Laptop for £999
//      Alice bought Book for £25
//      Bob bought Mouse for £49
//      Carol bought Pen for £3

4. Aggregate — Custom Reduction

Sum and Count are pre-built reductions; Aggregate is the general one — LINQ's reduce. It walks the sequence carrying an accumulator, applying your lambda (acc, item) => ... at each step. Always prefer the form that takes a seed (the starting accumulator): it lets the result be a different type from the items, and — crucially — it won't throw on an empty sequence the way the seedless form does.

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

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

        // Aggregate is LINQ's "reduce": it folds a sequence into ONE value.
        // It walks left to right, carrying an accumulator (acc) along the way.

        // Without a seed: acc starts as the FIRST item.
        int product = numbers.Aggregate((acc, n) => acc * n);
        Console.WriteLine($"Product: {product}");   // 2 * 3 * 4 = 24

        // With a SEED (the safer form): acc starts at the value you give.
        // This also lets the result be a different TYPE from the items.
        int sumPlus100 = numbers.Aggregate(100, (acc, n) => acc + n);
        Console.WriteLine($"Sum + 100: {sumPlus100}");   // 100 + 2 + 3 + 4 = 109

        // Build a sentence by folding strings together.
        var words = new[] { "C#", "is", "fun" };
        string sentence = words.Aggregate((acc, w) => acc + " " + w);
        Console.WriteLine(sentence);   // C# is fun

        // A seed protects you on an EMPTY sequence — the no-seed form throws.
        var empty = new List<int>();
        int safeTotal = empty.Aggregate(0, (acc, n) => acc + n);
        Console.WriteLine($"Safe total of empty: {safeTotal}");   // 0
    }
}

// ✅ Expected output:
//    Product: 24
//    Sum + 100: 109
//    C# is fun
//    Safe total of empty: 0

5. Skip / Take — Paging

When you can't show everything at once — search results, an admin table, an API endpoint — you page. Skip(n) jumps past the first n items; Take(n) keeps the next n. The classic formula is Skip((page - 1) * pageSize).Take(pageSize). Their cousins TakeWhile/SkipWhile work on a condition instead of a count, stopping at the first item that fails the test.

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

class Program
{
    static void Main()
    {
        // 23 results we want to show 5 per page (like search results).
        var items = Enumerable.Range(1, 23)
            .Select(i => $"Item {i}")
            .ToList();

        int pageSize = 5;

        for (int page = 1; page <= 3; page++)
        {
            // Skip the items on earlier pages, Take this page's worth.
            // Page 1 -> Skip 0 Take 5 ; Page 2 -> Skip 5 Take 5 ; ...
            var pageItems = items
                .Skip((page - 1) * pageSize)
                .Take(pageSize);

            Console.WriteLine($"--- Page {page} ---");
            Console.WriteLine("  " + string.Join(", ", pageItems));
        }
        // --- Page 1 ---  Item 1 .. Item 5
        // --- Page 2 ---  Item 6 .. Item 10
        // --- Page 3 ---  Item 11 .. Item 15

        // TakeWhile / SkipWhile stop (or start) at the first failing item.
        var ascending = new[] { 1, 2, 5, 9, 3, 8 };
        var rising = ascending.TakeWhile(n => n < 9);   // 1, 2, 5  (stops at 9)
        Console.WriteLine($"\nTakeWhile < 9: {string.Join(", ", rising)}");
    }
}

// ✅ Expected output:
//    --- Page 1 ---
//      Item 1, Item 2, Item 3, Item 4, Item 5
//    --- Page 2 ---
//      Item 6, Item 7, Item 8, Item 9, Item 10
//    --- Page 3 ---
//      Item 11, Item 12, Item 13, Item 14, Item 15
//
//    TakeWhile < 9: 1, 2, 5

6. Deferred Execution & Multiple Enumeration

This is the difference between a senior and a junior LINQ user. A query is deferred: defining it runs nothing — it's a recipe. The work happens every time you enumerate it (foreach, Count(), ToList()…). So enumerating the same query twice runs the whole pipeline twice — wasteful if it's expensive, and a real bug if the source changed in between. The fix is to materialise once with ToList() (or ToArray()) and reuse that snapshot. Watch the evaluation count in the example.

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

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

        // Defining a query runs NOTHING — it's just a stored recipe.
        var query = numbers.Where(n =>
        {
            Console.WriteLine($"  evaluating {n}");   // proves WHEN it runs
            return n > 2;
        });

        Console.WriteLine("Query defined — nothing has run yet.");
        Console.WriteLine("First enumeration:");
        int c1 = query.Count();        // runs the pipeline once
        Console.WriteLine($"Count = {c1}");

        // ⚠️ MULTIPLE ENUMERATION: each pass re-runs the WHOLE pipeline.
        Console.WriteLine("Second enumeration (runs it AGAIN):");
        var list = query.ToList();     // runs the pipeline a SECOND time

        // Fix: materialise ONCE with ToList(), then reuse the snapshot freely.
        var snapshot = numbers.Where(n => n > 2).ToList();   // runs now, frozen
        numbers.Add(99);                                     // won't affect snapshot
        Console.WriteLine($"\nSnapshot (no 99): {string.Join(", ", snapshot)}");
        // Snapshot (no 99): 3, 4, 5
    }
}

🔎 Deep Dive: ToList vs ToArray vs ToDictionary vs ToLookup

These four all force execution — they turn a lazy query into concrete, in-memory data. Which one you pick depends on how you'll use the result:

query.ToList();        // List<T>  — the everyday choice; you'll add/index it
query.ToArray();       // T[]      — fixed size, tiny bit leaner than a List
query.ToDictionary(    // Dictionary<K,V> — O(1) lookups by a UNIQUE key
    x => x.Id);        // ⚠ throws if two items share the same key
query.ToLookup(        // ILookup<K,V> — like a Dictionary but ONE key -> MANY
    x => x.Category);  // perfect when keys repeat; never throws on duplicates

Rule of thumb: reach for ToList() by default; ToDictionary when you'll look items up by a unique key; and ToLookup when keys repeat (it's essentially a materialised GroupBy you can index into).

7. Performance — Filter Early, Avoid Re-enumeration

LINQ is readable, but order still matters. Filter early so later steps process fewer items; project late so you only carry the fields you need; limit with Take so you stop as soon as you have enough. Prefer Any() over Count() > 0 — Any stops at the first match while Count scans everything. And when you look the same data up repeatedly, build a ToDictionary once (O(1) lookups) instead of calling Where/First in a loop (O(n) every time — the classic N+1 trap).

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

class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public bool InStock { get; set; }
}

class Program
{
    static void Main()
    {
        var products = Enumerable.Range(1, 1000).Select(i => new Product
        {
            Id = i,
            Name = $"Product {i}",
            Price = i * 1.5m,
            InStock = i % 4 != 0
        }).ToList();

        // ✅ Filter EARLY, project LATE, limit with Take — least work per item.
        var topTen = products
            .Where(p => p.InStock && p.Price > 100)   // shrink the set first
            .OrderBy(p => p.Price)                     // sort the smaller set
            .Select(p => new { p.Name, p.Price })      // keep only needed fields
            .Take(10);                                  // stop after 10

        Console.WriteLine("=== 10 cheapest in-stock products over £100 ===");
        foreach (var p in topTen)
            Console.WriteLine($"  {p.Name}: £{p.Price}");

        // Any() short-circuits at the FIRST match — Count() > 0 scans everything.
        bool hasExpensive = products.Any(p => p.Price > 1000);
        Console.WriteLine($"\nHas item over £1000? {hasExpensive}");

        // ToDictionary = O(1) lookups by key, vs Where() which is O(n) each time.
        var byId = products.ToDictionary(p => p.Id);
        Console.WriteLine($"Product 500: {byId[500].Name} (£{byId[500].Price})");
    }
}

// ✅ Expected output:
//    === 10 cheapest in-stock products over £100 ===
//      Product 67: £100.5
//      Product 69: £103.5
//      Product 70: £105.0
//      Product 71: £106.5
//      Product 73: £109.5
//      Product 74: £111.0
//      Product 75: £112.5
//      Product 77: £115.5
//      Product 78: £117.0
//      Product 79: £118.5
//
//    Has item over £1000? True
//    Product 500: Product 500 (£750.0)

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeResult
Group + countitems.GroupBy(x => x.Cat)groups w/ .Key
Sum per groupg.Sum(x => x.Amount)a number
Flattenitems.SelectMany(x => x.Sub)flat sequence
Joina.Join(b, x => x.Id, y => y.AId, ...)matched pairs
Reduceitems.Aggregate(0, (a, x) => a + x)one value
Pageitems.Skip(10).Take(10)items 11–20
Snapshotquery.ToList()List<T>
Key lookupitems.ToDictionary(x => x.Id)O(1) lookups

Frequently Asked Questions

Q: When should I use GroupBy versus ToLookup?

Both bucket items by a key. GroupBy is deferred — it re-groups every time you enumerate it. ToLookup runs immediately and gives you an indexable ILookup<K, V> you can reuse. Use ToLookup when you'll hit the same buckets repeatedly; GroupBy when it's a one-pass summary.

Q: What's the real difference between Select and SelectMany?

Select gives you one output per input — if each input is a list, you get a list of lists. SelectMany flattens those inner lists into one continuous sequence. Reach for it whenever a Select would leave you with nested collections.

Q: My query seems slow / runs twice — why?

LINQ is lazy, so enumerating the same query object more than once re-executes the entire pipeline each time. Call .ToList() once to materialise the result, then iterate the list as often as you like.

Q: When do I need a seed in Aggregate?

Use a seed whenever the result type differs from the item type, or whenever the sequence might be empty. The seedless overload throws on an empty sequence; Aggregate(0, ...) just returns the seed.

Mini-Challenge: Spend Per Account

No blanks this time — just a brief and an outline to keep you on track. Starting from the list of transactions, GroupBy the account name, Sum each group's amount, and print one line per account. Then add the bonus: OrderByDescending the total so the biggest account leads. Run it and check your output against the expected lines in the comments.

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

class Transaction
{
    public string Account { get; set; }
    public decimal Amount { get; set; }
}

class Program
{
    static void Main()
    {
        var transactions = new List<Transaction>
        {
            new Transaction { Account = "Savings", Amount = 200 },
            new Transaction { Account = "Current", Amount = 50  },
            new Transaction { Account = "Savings", Amount = 125 },
            new Transaction { Account = "Current", Amount = 75  },
            new Transaction { Account = "Bonus",   Amount = 500 },
        };

        // 🎯 MINI-CHALLENGE: total spend per account
        // 1. GroupBy the Account name.
        // 2. For each group, Sum the Amount of its transactions.
        // 3. foreach over the groups and print:  "Account: £Total"
        // 4. BONUS: OrderByDescending the total so the biggest account is first.
        //
        // ✅ Expected output (bonus order):
        //    Bonus: £500
        //    Savings: £325
        //    Current: £125

        // your code here
    }
}

🎉 Lesson Complete

Practice quiz

What does GroupBy return for each bucket?

  • A single number
  • An IGrouping that has a .Key and is itself a sequence of its items
  • A sorted list
  • A Dictionary

Answer: An IGrouping that has a .Key and is itself a sequence of its items. Each group is an IGrouping<K,T>: it carries a .Key and is itself a sequence, so you can call Count(), Sum(), or Average() on it.

What does SelectMany do that Select does not?

  • Sorts the sequence
  • Flattens nested sequences into one continuous sequence
  • Removes duplicates
  • Groups items by key

Answer: Flattens nested sequences into one continuous sequence. If each item is itself a list, Select gives a list of lists; SelectMany flattens those inner sequences into one continuous stream.

What kind of join does the LINQ Join operator perform?

  • A left outer join
  • A full outer join
  • An inner join — items with no match on the other side are dropped
  • A cross join

Answer: An inner join — items with no match on the other side are dropped. Join matches rows from two sources on a shared key, like a SQL inner join; unmatched items on either side are dropped.

Why should you prefer the seeded form Aggregate(seed, ...)?

  • It runs faster
  • It is empty-safe and lets the result be a different type from the items
  • It sorts the result
  • It is the only form that compiles

Answer: It is empty-safe and lets the result be a different type from the items. The seeded form won't throw on an empty sequence (the seedless form does) and lets the accumulator/result type differ from the item type.

What is the classic paging formula with Skip and Take?

  • Take(page).Skip(pageSize)
  • Skip(page * pageSize).Take(page)
  • Skip((page - 1) * pageSize).Take(pageSize)
  • Take(pageSize).Skip(pageSize)

Answer: Skip((page - 1) * pageSize).Take(pageSize). Skip((page - 1) * pageSize).Take(pageSize) jumps past earlier pages and takes the current page's worth of items.

What happens if you enumerate the same deferred query twice?

  • The second enumeration is free (cached)
  • The whole pipeline re-runs each time
  • It throws an exception
  • Only the first item is recomputed

Answer: The whole pipeline re-runs each time. A deferred query is a recipe; each enumeration re-runs the entire pipeline. Materialise once with ToList() to avoid the waste.

Why is Any() generally better than Count() > 0 to test for matches?

  • Any() is more readable only
  • Any() stops at the first match, while Count() scans everything
  • Count() throws on empty sequences
  • Any() sorts the data first

Answer: Any() stops at the first match, while Count() scans everything. Any() short-circuits at the first matching item; Count() > 0 scans the whole sequence to produce a total it doesn't need.

What does ToDictionary throw on if two items share the same key?

  • NullReferenceException
  • InvalidOperationException
  • ArgumentException ('An item with the same key has already been added')
  • Nothing — it overwrites

Answer: ArgumentException ('An item with the same key has already been added'). ToDictionary requires unique keys and throws ArgumentException on a duplicate. Use ToLookup for one-key-to-many values.

When should you use ToLookup instead of ToDictionary?

  • When keys are unique
  • When keys repeat (one key maps to many values)
  • When you need O(n) lookups
  • When the sequence is empty

Answer: When keys repeat (one key maps to many values). ToLookup builds an ILookup where one key maps to many values and never throws on duplicates — essentially a materialised GroupBy you can index.

Where do items with a null grouping key end up in a GroupBy?

  • They are dropped from the result
  • They throw an exception
  • They all land in a single group whose Key is null
  • They are spread across all groups

Answer: They all land in a single group whose Key is null. A null key sweeps every null-keyed item into one group with a null Key. Coalesce the key (e.g. o.Category ?? "Unknown") if that's not what you want.

Continue this course

Related lessons