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.

