Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Module 1 · LINQ to Objects · Lesson 3 of 4

    Select vs SelectMany in LINQ to Objects: projection or flattening

    Watch

    Learning outcome

    By the end of this lesson, you should be able to choose Select when you want a one-to-one projection and SelectMany when each source element produces zero, one, or many results that should be flattened into a single sequence. In interviews, the key signal is not “which one looks shorter,” but “what shape should the final sequence have?”

    Intuition

    Think of Select as a mapper: for every item in the source, it creates exactly one result element in the output sequence. The output therefore keeps the same outer shape as the input.

    For example, use Select to project each word to its length. The result is still a sequence of the same count as the input.

    By contrast, SelectMany is for nested results. If each outer item contains an inner sequence, and you want one flat sequence containing all inner items, use SelectMany.

    For example, flatten the tag collections with SelectMany; each post can contribute multiple tags, and the result is one flattened list of tags.

    Deep dive

    In LINQ to Objects, both operators are deferred: they build an iterator pipeline and only run when you enumerate the result. The difference is the shape of the iteration.

    • Select: one source element -> one projected element
    • SelectMany: one source element -> many inner elements, flattened into one output stream

    A common interview framing is:

    • Use Select when you want to transform each item into another item.
    • Use SelectMany when you want to transform each item into a collection and then concatenate all those collections.

    A practical distinction is nesting depth. If your projection returns a scalar or a single object, Select is appropriate. If your projection returns IEnumerable<T> and you want to remove one layer of nesting, SelectMany is usually the right operator.

    Projecting each customer to their Orders collection with Select produces a sequence of sequences. Flattening those collections with SelectMany produces a single sequence of orders.

    Another useful rule: SelectMany can also carry context from the outer element into the flattened result by using its result selector overload. That is helpful when you need both the parent and child values.

    The key interview insight is that SelectMany is not “advanced Select.” It solves a different shape problem.

    Failure modes

    A frequent mistake is using Select when the projection returns a collection but the required output is flat. That creates a nested sequence. Select is still the right choice when preserving those inner collections is intentional.

    Another mistake is using SelectMany when you only need one output per input. That can hide a simpler intent and make debugging harder.

    Be careful not to confuse LINQ to Objects with database translation. In-memory LINQ executes actual delegates over objects. In database-backed LINQ providers, query translation may rewrite expressions into SQL, but this lesson is about object sequences only.

    Also watch out for accidental multiple enumeration of the same inner sequence if the projected collections are deferred. If the inner sequence is expensive or stateful, flattening can expose those costs more clearly.

    Interview edge cases: preserve the intended rows

    The practice examples use LINQ to Objects and modern C# (C# 9 or later for top-level programs), with the usual System, System.Collections.Generic and System.Linq imports. Unless a question says otherwise, inputs are non-null and are not modified concurrently.

    • Flattening retains duplicate values. It removes one nesting level, whereas uniqueness requires a separate rule. Strings are enumerable as characters, so SelectMany(word => word) produces characters rather than whole words.
    • Null is not an empty collection. If the domain treats null children as no children, use an explicit empty-sequence fallback. A null-forgiving ! changes compiler analysis, not the runtime value.
    • An empty child collection normally contributes no flattened rows. To retain a parent with a missing-child placeholder, apply DefaultIfEmpty to its inner sequence and use the result selector. For nullable integer child IDs, convert with Select(id => (int?)id) before DefaultIfEmpty; otherwise an empty int sequence supplies 0 instead of null.
    • Filtering parents with Any does not filter their children. An order with one paid line can pass the parent test and still contribute unpaid lines when all its lines are flattened. Apply the paid-line condition inside the collection selector or after flattening.
    • An inner sequence independent of its parent produces every combination: two sizes and three colors yield six size-color pairs. Verify whether that multiplicity is intended.
    • Indexed Select numbers its immediate input starting at zero. After Where it numbers surviving elements, not their original source positions. Attach the index before filtering when original positions must survive.

    Interview drill

    1. You have List<Student> and each student has List<string> Courses. Which operator gives you one flat list of all courses?
    2. You have List<Product> and want List<decimal> of prices. Which operator fits best?
    3. You need { StudentName, CourseName } for every student-course pair. Which operator and why?
    4. If a candidate says “SelectMany is for joins,” how would you correct that statement?

    Expected answers:

    • 1: SelectMany because you are flattening nested course lists.
    • 2: Select because each product maps to one price.
    • 3: SelectMany with a result selector, because each student produces many pairs.
    • 4: It is more general than joins; it flattens nested sequences, and joins are just one possible use case.

    Revision checklist

    • I can explain Select as one-to-one projection.
    • I can explain SelectMany as flattening nested sequences.
    • I can predict whether the result is nested or flat.
    • I can choose the operator based on desired output shape, not syntax familiarity.
    • I can distinguish LINQ to Objects behavior from query provider translation.

    Production code

    The example below shows both operators side by side over in-memory objects. Notice how Select preserves the nested shape, while SelectMany produces one flat sequence.

    Code walkthrough

    The employeeCounts query uses Select because each department becomes exactly one anonymous result with a name and a count. Even though the count is derived from a collection, the output is still one item per department.

    The allEmployees query uses SelectMany because each department contributes zero or more employees, and the final goal is a single flat stream of employee names. The empty support department contributes nothing, which is a normal and useful result of flattening.

    The labeledEmployees query demonstrates the result-selector overload of SelectMany. It is especially useful when you want to flatten child data while still retaining information from the parent object.

    A good interview summary is:

    • Select maps each source element to one result; the result may have the same type or a different type.
    • SelectMany changes the sequence shape.

    If you remember that distinction, you will usually choose correctly under pressure.

    Output: Select vs SelectMany over nested in-memory data

    Select keeps one result per department: Engineering -> 2 Design -> 1 Support -> 0

    SelectMany flattens all employees: Ada Grace Edsger

    SelectMany can keep parent context too: Engineering:Ada Engineering:Grace Design:Edsger

    Executable code examples

    Select vs SelectMany over nested in-memory data

    Program.cs

    C#Runs
    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    var departments = new List<Department>
    {
        new("Engineering", new List<string> { "Ada", "Grace" }),
        new("Design", new List<string> { "Edsger" }),
        new("Support", Array.Empty<string>())
    };
    
    var employeeCounts = departments.Select(d => new { d.Name, Count = d.Employees.Count });
    var allEmployees = departments.SelectMany(d => d.Employees);
    var labeledEmployees = departments.SelectMany(
        d => d.Employees,
        (department, employee) => $"{department.Name}:{employee}");
    
    Console.WriteLine("Select keeps one result per department:");
    foreach (var item in employeeCounts)
    {
        Console.WriteLine($"{item.Name} -> {item.Count}");
    }
    
    Console.WriteLine();
    Console.WriteLine("SelectMany flattens all employees:");
    foreach (var employee in allEmployees)
    {
        Console.WriteLine(employee);
    }
    
    Console.WriteLine();
    Console.WriteLine("SelectMany can keep parent context too:");
    foreach (var item in labeledEmployees)
    {
        Console.WriteLine(item);
    }
    
    public sealed record Department(string Name, IReadOnlyList<string> Employees);

    Optional video

    Gavin Lon / freeCodeCamp.org · Watch 3:00:25–3:03:46 for Select and SelectMany. Watching is optional.

    Watch on YouTube.

    Label the boxes or unpack them

    Imagine departments as labeled boxes, with employee cards inside. Select produces one result for each box: perhaps its name and card count, or the box’s collection of cards. SelectMany unpacks the cards into one flat stream. An empty box contributes no cards.

    If every card must keep its department label, use the result selector to copy that parent context into each output row. Unpacking does not remove duplicate cards, and deciding to keep a box does not remove unwanted cards inside it.

    Start with the required output shape

    • One result per source item: Select. The element type may stay the same or change.
    • One flat stream from child collections: SelectMany.
    • Parent plus child fields: use the SelectMany result-selector overload.
    • Empty children normally produce zero rows; use an explicit placeholder policy when parents must remain.
    • For a missing integer child ID represented by null, project to int? before DefaultIfEmpty().
    • Null children need an explicit domain policy; ! does not create an empty sequence.
    • Flattening keeps duplicates, and flattening strings produces characters.
    • Filter parents and children at the intended grain. Indexed Select counts the sequence reaching that operator.

    Practice

    Sign in to mark lessons done and keep your place in the course.Sign in