Projection and shaping results
Select and SelectMany decide the shape of the data, and projecting in the query rather than after it is the cheapest performance work in LINQ.
Select and SelectMany
// Select: one output element per input element
var names = people.Select(p => p.Name);
// Select with an index
var numbered = people.Select((p, i) => (Index: i + 1, p.Name));
// SelectMany: flatten one level, producing zero or more outputs per input
var allOrders = customers.SelectMany(c => c.Orders);
var pairs = new[] { "ab", "cd" }.SelectMany(s => s.ToCharArray());
// The query syntax form of SelectMany is a second from clause
var allOrders2 = from c in customers
from o in c.Orders
select o;
// SelectMany with a result selector, which keeps the parent in scope
var rows = customers.SelectMany(
c => c.Orders,
(c, o) => new { c.Name, o.Id, o.Total });
// Flatten a dictionary in one pass
var entries = dict.SelectMany(
kvp => kvp.Value,
(kvp, v) => new { Key = kvp.Key, Value = v });Choosing the output shape
// Anonymous type: convenient inside one method, cannot cross a boundary
var anon = orders.Select(o => new { o.Id, o.Total });
// A record or a named DTO is what you want at a boundary
public sealed record OrderRow(int Id, decimal Total, string Customer);
var rows = orders
.Where(o => o.PlacedAt >= since)
.Select(o => new OrderRow(o.Id, o.Total, o.Customer.Name))
.ToList();
// Projection pushes the column list into the SQL: only these columns are read
var slim = db.Orders
.Where(o => o.Status == OrderStatus.Open)
.Select(o => new { o.Id, o.Total })
.ToListAsync(ct);
// A grouped shape, built in one pass instead of a second query
var byCustomer = orders
.GroupBy(o => o.CustomerId)
.Select(g => new CustomerSummary(
g.Key, g.Count(), g.Sum(o => o.Total)))
.ToList();
// A tuple is fine for an internal return value
private static (int Count, decimal Total) Summarise(IEnumerable<Order> orders) =>
(orders.Count(), orders.Sum(o => o.Total));| Shape | Use it when | Cost or risk |
|---|---|---|
| Anonymous type | Inside one method, immediately consumed | Cannot be returned from a public API |
| Record | Crossing a boundary, or needing equality | An allocation per element if it is a class |
record struct | Small value-shaped projections in a loop | Copied on every assignment |
| Tuple | Returning two or three related values | Field names are lost across an assembly |
| Domain entity | You genuinely need the tracked entity | Reads every column and tracks every object |
- Projecting into a DTO before
ToListAsyncmakes the database return only the columns you use, which is often the largest single performance win available. Select(o => o)is not a no-op on a queryable source: it can still force a full entity materialisation and change tracking.- An anonymous type cannot be used as a return type, a parameter or a field. The compiler refuses, and that is the intended design.
- Projecting after materialising means every column of every row crossed the network first. The order of Select and ToList matters enormously on a queryable source.
⚠️
Adding a method call inside Select that the provider cannot translate silently moves the work to the client. On a large table that means downloading every row before applying your function, and the query still looks fast in a unit test over a list.
FAQ
What is the difference between Select and SelectMany?
Select produces exactly one output per input. SelectMany produces a flattened sequence of zero or more outputs per input, which is why it is the operator for nested collections.
Should I project into a record or reuse the entity?
Project into a purpose-built type for reads. Reusing the entity couples the query shape to the schema and pulls change tracking into code that only wanted to read.
Related
Common LINQ operators Grouping, joining and set operations
Last refreshed 2026-09-18.