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));
ShapeUse it whenCost or risk
Anonymous typeInside one method, immediately consumedCannot be returned from a public API
RecordCrossing a boundary, or needing equalityAn allocation per element if it is a class
record structSmall value-shaped projections in a loopCopied on every assignment
TupleReturning two or three related valuesField names are lost across an assembly
Domain entityYou genuinely need the tracked entityReads every column and tracks every object
  • Projecting into a DTO before ToListAsync makes 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.

Common LINQ operators Grouping, joining and set operations

Last refreshed 2026-09-18.