BrandGhost
LINQ Projection in C#: Select, SelectMany, and Flattening Collections

LINQ Projection in C#: Select, SelectMany, and Flattening Collections

LINQ projection in C# is how you transform data from one shape into another without writing verbose foreach loops and temporary lists. The Select and SelectMany operators sit at the heart of every data pipeline -- whether you're mapping database records to DTOs, flattening a product catalog's nested category tree, or building read-model projections for a query handler. Understanding the difference between Select and SelectMany, knowing when to use anonymous types vs records, and leveraging the .NET 9 Index() method will make your LINQ projection code cleaner and more expressive.

This article covers every major projection pattern with realistic domain examples, before/after comparisons for .NET 9 additions, and clear guidance on which tool to reach for in each scenario.


Select(selector): Transforming Individual Elements

Select maps every element in a source sequence through a transformation function, producing a new sequence of the same length. It is a one-to-one mapping -- one input element produces exactly one output element.

namespace ECommerce.Catalog;

public sealed record Product(
    string Sku,
    string Name,
    decimal Price,
    string CategoryName);

// Project to a single value
IEnumerable<string> skus = products.Select(p => p.Sku);
IEnumerable<decimal> prices = products.Select(p => p.Price);

// Project to an anonymous type
var summaries = products.Select(p => new
{
    p.Sku,
    p.Name,
    DiscountedPrice = p.Price * 0.9m
});

// Project to a named record
public sealed record ProductSummary(string Sku, string Name, decimal DiscountedPrice);

IEnumerable<ProductSummary> dtos = products.Select(p =>
    new ProductSummary(p.Sku, p.Name, p.Price * 0.9m));

Select is lazy -- the transformation function is not called until you iterate the result. This means you can compose multiple Select calls in a pipeline without materializing intermediate collections.


Select with Index: Enumerating Position

Select has an overload that provides the index of each element alongside the element itself. This is useful when position in the sequence carries meaning -- rendering numbered lists, pairing items with ordinal identifiers, or detecting the first/last item during transformation.

namespace ECommerce.Catalog;

// Before .NET 9 -- Select overload with index
IEnumerable<string> numberedNames =
    products.Select((p, i) => $"{i + 1}. {p.Name}");

// Project to a tuple with position
IEnumerable<(int Position, string Sku)> positioned =
    products.Select((p, i) => (i, p.Sku));

// Find first and last without Count()
var withIndex = products.Select((p, i) => (Index: i, Product: p)).ToList();
var first = withIndex.First();
var last = withIndex.Last();

.NET 9: Index() Method

.NET 9 introduces Index() as a first-class operator that replaces the Select((item, i) => (i, item)) idiom:

namespace ECommerce.Catalog;

// Before .NET 9 -- verbose tuple construction
IEnumerable<(int Index, Product Item)> indexedOld =
    products.Select((p, i) => (i, p));

// .NET 9 -- Index() is clean and self-documenting
IEnumerable<(int Index, Product Item)> indexed = products.Index();

// Practical use: render a ranked product list
foreach (var (i, product) in products.OrderByDescending(p => p.Price).Index())
{
    Console.WriteLine($"#{i + 1}: {product.Name} -- {product.Price:C}");
}

Index() makes intent explicit -- it says "I want this sequence with its indices" without the noise of a lambda that just pairs the element with its position.


SelectMany: Flattening Nested Collections

SelectMany maps each element to a sub-collection and then flattens all sub-collections into a single output sequence. It is a one-to-many mapping -- one input element can produce zero, one, or many output elements.

namespace ECommerce.Catalog;

public sealed record Category(string Name, IReadOnlyList<Product> Products);

// Select (wrong for this) -- produces IEnumerable<IReadOnlyList<Product>>
IEnumerable<IReadOnlyList<Product>> nestedProducts =
    categories.Select(c => c.Products);

// SelectMany -- flattens into IEnumerable<Product>
IEnumerable<Product> allProducts =
    categories.SelectMany(c => c.Products);

// Flatten order line items from all orders
IEnumerable<OrderLineItem> allLineItems =
    orders.SelectMany(o => o.LineItems);

// Flatten tags from a product -- tags are a collection property
IEnumerable<string> allTags =
    products.SelectMany(p => p.Tags);

The key mental model: Select produces one output per input (same length). SelectMany can produce any number of outputs per input (different length, flattened).


SelectMany with a Result Selector

The SelectMany overload that accepts a result selector lets you project the flattened sub-elements while keeping context from the parent element:

namespace ECommerce.Catalog;

// Flatten products while retaining the category name
IEnumerable<(string CategoryName, Product Product)> flat =
    categories.SelectMany(
        c => c.Products,
        (c, p) => (c.Name, p));

// Flatten order line items while retaining the order context
IEnumerable<(int OrderId, string CustomerId, OrderLineItem LineItem)> lineItemsWithContext =
    orders.SelectMany(
        o => o.LineItems,
        (o, li) => (o.Id, o.CustomerId, li));

// Build shipping tasks: one task per line item, per order
var shippingTasks =
    orders
        .Where(o => o.Status == OrderStatus.Processing)
        .SelectMany(
            o => o.LineItems,
            (o, li) => new ShippingTask(o.Id, o.ShippingAddress, li.Sku, li.Quantity));

The result selector pattern is significantly more readable than the equivalent Select + SelectMany chain because the parent context is available directly without needing a separate anonymous type to carry it forward.


Chaining Projections

Select calls can be chained freely. Because each Select is deferred, chaining them composes the transformations without allocating intermediate collections:

namespace ECommerce.Reporting;

public sealed record OrderLineItem(string Sku, int Quantity, decimal UnitPrice);
public sealed record Order(
    int Id,
    string CustomerId,
    IReadOnlyList<OrderLineItem> LineItems);

// Multi-step pipeline: no intermediate allocations until ToList()
var revenueByProduct =
    orders
        .SelectMany(o => o.LineItems)          // flatten to line items
        .Select(li => new                       // project to key+value
        {
            li.Sku,
            Revenue = li.Quantity * li.UnitPrice
        })
        .GroupBy(x => x.Sku)                   // group by product
        .Select(g => new                        // aggregate per product
        {
            Sku = g.Key,
            TotalRevenue = g.Sum(x => x.Revenue)
        })
        .OrderByDescending(x => x.TotalRevenue)
        .ToList();

This pipeline is evaluated in a single pass -- the SelectMany, Select, GroupBy, and final Select are all composed before ToList() triggers execution.


Anonymous Types vs Records: When to Use Each

Choosing between anonymous types and named records for projection targets depends on how far the projected type travels:

Use anonymous types when:

  • The projection is local to a single method
  • You need a temporary shape for a further LINQ operation (GroupBy key, intermediate join, etc.)
  • The type never crosses a method boundary or is never stored in a field

Use named records when:

  • The projected type is a method return value
  • The type represents a read model, DTO, or API response
  • You need XML documentation, validation, or serialization attributes
  • The type is used in tests, making the test assertions readable
namespace ECommerce.Queries;

// Anonymous type -- fine for internal pipeline use
var internalStep = orders.Select(o => new
{
    o.Id,
    LineCount = o.LineItems.Count
});

// Named record -- correct for a method return value or DTO
public sealed record OrderSummaryDto(int Id, string CustomerId, int LineCount, decimal Total);

IEnumerable<OrderSummaryDto> GetOrderSummaries(IEnumerable<Order> orders) =>
    orders.Select(o => new OrderSummaryDto(
        o.Id,
        o.CustomerId,
        o.LineItems.Count,
        o.LineItems.Sum(li => li.Quantity * li.UnitPrice)));

In a CQRS with Feature Slices architecture, each query handler defines its own response record. The Select projection that produces that record lives entirely within the handler -- no shared DTO bleeding across feature boundaries. This keeps projections focused and easy to evolve independently.


Practical: Projecting for a Read Model

A common real-world projection scenario is building a read model from domain entities. The Select operator lets you project to a flat, denormalized shape purpose-built for display:

namespace ECommerce.ReadModels;

public sealed record OrderDetailViewModel(
    int OrderId,
    string CustomerName,
    string CustomerEmail,
    IReadOnlyList<LineItemViewModel> LineItems,
    decimal Total,
    string StatusLabel);

public sealed record LineItemViewModel(
    string Sku,
    string ProductName,
    int Quantity,
    decimal UnitPrice,
    decimal LineTotal);

public static IEnumerable<OrderDetailViewModel> BuildOrderDetailViews(
    IEnumerable<Order> orders,
    IReadOnlyDictionary<string, Customer> customerById,
    IReadOnlyDictionary<string, string> productNameBySku)
{
    return orders.Select(o =>
    {
        var customer = customerById[o.CustomerId];
        var lineItems = o.LineItems
            .Select(li => new LineItemViewModel(
                li.Sku,
                productNameBySku.GetValueOrDefault(li.Sku, li.Sku),
                li.Quantity,
                li.UnitPrice,
                li.Quantity * li.UnitPrice))
            .ToList();

        return new OrderDetailViewModel(
            o.Id,
            customer.Name,
            customer.Email ?? string.Empty,
            lineItems,
            lineItems.Sum(l => l.LineTotal),
            o.Status.ToDisplayLabel());
    });
}

This pattern -- projecting domain objects to view models in a single, composable Select -- is common in Feature Slicing in C#, where each feature slice contains its own projection logic rather than relying on a global mapper.


Projecting for Semantic Search and AI Pipelines

Projection is not limited to UI concerns. When building AI-powered features, Select and SelectMany commonly extract text fields or build embedding input vectors from domain objects:

namespace ECommerce.Search;

// Extract text for embedding -- flatten title, description, and tags into one string
IEnumerable<(int ProductId, string EmbeddingInput)> embeddingInputs =
    products.Select(p => (
        p.Id,
        EmbeddingInput: string.Join(" ", new[]
        {
            p.Name,
            p.Description,
            string.Join(" ", p.Tags)
        })));

// SelectMany all searchable text tokens
IEnumerable<string> allSearchableTokens =
    products.SelectMany(p =>
        p.Tags.Append(p.Name).Append(p.CategoryName));

For a complete example of integrating LINQ-based pipelines with AI search, see Build a Semantic Search Engine with Semantic Kernel in C# -- LINQ projection is used throughout to shape the input data for embedding and retrieval.


Common Mistakes with Select and SelectMany

Mistake 1: Using Select when SelectMany is needed

namespace ECommerce.Catalog;

// Wrong -- IEnumerable<IReadOnlyList<Product>>, NOT IEnumerable<Product>
var nestedResult = categories.Select(c => c.Products);

// Correct -- IEnumerable<Product>
var flatResult = categories.SelectMany(c => c.Products);

The type signature tells you which you need: if Select would return IEnumerable<IEnumerable<T>>, use SelectMany instead.

Mistake 2: Calling ToList() inside Select (allocating per element)

namespace ECommerce.Orders;

// Expensive -- creates a new List<T> per element, forcing enumeration of each sub-collection
var eagerly = orders.Select(o => o.LineItems.ToList());

// Better -- keep lazy unless callers truly need mutable lists
var lazily = orders.Select(o => o.LineItems); // returns IReadOnlyList already

Mistake 3: Forgetting that Select is deferred

namespace ECommerce.Reporting;

var projectedQuery = orders.Select(o => ExpensiveTransform(o));

// ExpensiveTransform is called here (deferred)
foreach (var result in projectedQuery) { ... }

// ExpensiveTransform is called AGAIN if you iterate a second time
int count = projectedQuery.Count(); // second full pass through all transforms

If ExpensiveTransform has side effects or is computationally heavy, materialize with ToList() before iterating more than once.

The Factory Method Pattern pairs well here -- instead of embedding the construction logic inline in your Select lambda, encapsulate it in a factory method. This makes the projection testable in isolation and keeps the query chain readable.


Frequently Asked Questions

What is the difference between Select and SelectMany in LINQ?

Select is a one-to-one mapping -- each input element produces exactly one output element. The output sequence has the same length as the input. SelectMany is a one-to-many mapping -- each input element can produce zero, one, or many output elements, and all sub-sequences are flattened into a single output sequence. Use Select to transform elements; use SelectMany to flatten nested collections.

When should I use anonymous types vs named records in LINQ projections?

Use anonymous types for intermediate, local projections that stay within a single method (e.g., intermediate GroupBy keys, temporary pipeline shapes). Use named records when the projected type crosses a method boundary, represents a DTO or view model, is part of a public API, or needs to be tested by name. Named records also produce cleaner test assertion code than anonymous types.

What does the .NET 9 Index() method replace?

Index() replaces the pattern source.Select((item, i) => (i, item)). It returns a sequence of (int Index, T Item) value tuples for every element in the source. It makes intent explicit and removes the low-signal lambda from the call site. Available from .NET 9 / C# 13 onwards.

Does Select preserve order?

Yes. Select always preserves the input order -- the i-th output element is the projection of the i-th input element. This holds for LINQ to Objects. For LINQ to SQL/EF, the order of results depends on the generated query and whether an OrderBy clause is applied.

Can I nest Select and SelectMany in the same query?

Yes, and this is common for multi-level hierarchies. For a tree with Region -> Store -> Product, you can chain SelectMany(r => r.Stores).SelectMany(s => s.Products) to flatten to the leaf level, then Select to project each product into a view model. Each layer of nesting adds one SelectMany; the final projection is a Select.

Is SelectMany lazy?

Yes, both Select and SelectMany are deferred. No elements are enumerated until you iterate the result. Sub-collection access happens lazily per parent element as the caller walks the output sequence. If the sub-collection is expensive to produce (a database call, an I/O operation), be mindful of how many parent elements you process without materializing.

How do I project a flat sequence back into a nested structure?

Use GroupBy combined with Select to reconstruct hierarchy from a flat projection:

IEnumerable<Category> rebuilt =
    flatProducts
        .GroupBy(p => p.CategoryName)
        .Select(g => new Category(g.Key, g.ToList()));

This is the inverse of SelectMany -- SelectMany flattens; GroupBy + Select rebuilds.


Conclusion

LINQ projection in C# with Select and SelectMany covers the vast majority of data transformation needs in .NET applications. Select handles one-to-one transformations cleanly, including with index via the new .NET 9 Index() method. SelectMany flattens nested collections and, with its result selector overload, lets you carry parent context into the flattened output. Choosing named records over anonymous types for anything that crosses a method boundary, and being aware of deferred execution when composing chains, will keep your projection code both readable and efficient.

Weekly Recap: LINQ in C#, Design Patterns, and AI in Software Engineering [May 2026]

This week takes a deep tour of LINQ in C# covering grouping, aggregation, set operations, joins, deferred execution, and element access, plus complete guides to the proxy, flyweight, and bridge design patterns. The video lineup tackles context switching, generational dynamics on engineering teams, and where AI tooling actually fits in a real developer workflow.

LINQ in C#: Complete Guide to Language Integrated Query (.NET 6-9)

Master LINQ in C# with this complete guide covering filtering, projection, ordering, grouping, joins, and every new operator added in .NET 6 through .NET 10.

Weekly Recap: LINQ, C# Regex, and Design Pattern Deep Dives [May 2026]

This week brings deep coverage of LINQ in C# (filtering, projection, ordering, and the complete guide), advanced regex topics including named capture groups and pattern syntax, plus practical guides on the bridge, facade, and flyweight design patterns. Plus four new videos covering platform team work, agentic systems, and developer mindset.

An error has occurred. This application may no longer respond until reloaded. Reload