Projection
By the end of this lesson
Select only the columns you need instead of loading whole entities.
Projection means asking the database for a specific shape rather than a whole entity. In LINQ that is Select.
A read path almost never needs every column. An order list screen shows a date, a customer name and a total; it does not show the shipping address, the internal notes or the audit fields. Projection is how you say so, and it is usually the right default for anything that only displays data.
// Loads every mapped column of Order, plus every column of Customer.
List<Order> entities = await context.Orders
.Include(o => o.Customer)
.Where(o => o.Status == OrderStatus.Open)
.ToListAsync();
// The shape the screen actually needs.
public record OrderListItem(int Id, DateTime OrderDate, string CustomerName, decimal Total);
List<OrderListItem> rows = await context.Orders
.Where(o => o.Status == OrderStatus.Open)
.Select(o => new OrderListItem(
o.Id,
o.OrderDate,
o.Customer.Name,
o.Items.Sum(i => i.Quantity * i.UnitPrice)))
.ToListAsync();- The projection never mentions Include. Navigating to o.Customer.Name inside the Select is enough for EF Core to add the join it needs, and nothing more.
- o.Items.Sum(...) is translated into SQL and computed by the database. The order items never travel to your process at all.
- A record is convenient here because the type exists only to carry four values. An ordinary class works identically; so does an anonymous type if the result never leaves the method.
- Both queries filter on Status in the database. Projection changes what comes back, not what gets filtered.
-- From the entity query
SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate], [o].[Status],
[o].[ShippingAddress], [o].[Notes], [o].[PlacedByEmployeeId],
[c].[Id], [c].[Name], [c].[Email], [c].[Phone], [c].[BillingAddress]
FROM [Orders] AS [o]
INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
WHERE [o].[Status] = 1;
-- From the projection
SELECT [o].[Id], [o].[OrderDate], [c].[Name],
COALESCE((
SELECT SUM([i].[Quantity] * [i].[UnitPrice])
FROM [OrderItems] AS [i]
WHERE [o].[Id] = [i].[OrderId]), 0.0) AS [Total]
FROM [Orders] AS [o]
INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
WHERE [o].[Status] = 1;- Thirteen columns against three and a subquery. On a table where Notes holds a few kilobytes per row, that difference is most of the response time.
- The COALESCE is EF Core protecting the C# type: SUM over no rows is NULL in SQL, and Total is a non-nullable decimal.
- The total is now computed by the database. In the entity version you would fetch every order item and add them up in your process, which moves far more data to do the same arithmetic.
Choosing between them is mostly a question of what happens to the result next:
| Loading the entity | Projecting to a DTO | |
|---|---|---|
| Columns fetched | Every mapped column, plus every column of each Include | Only the ones you named |
| Change tracking | Tracked by default, ready to modify and save | Nothing to track |
| Can you save a change to it? | Yes | No — a DTO is not an entity |
| Related data | Include, with its join and its repeated parent rows | Navigate inside Select; EF Core adds only the join it needs |
| Aggregates such as a total | Fetch the children and add them up in memory | Computed by the database, returned as one value |
| Suits | Write paths: load, change, save | Read paths: lists, reports, API responses |
Summary
- Projection asks the database for a shape instead of an entity, using Select
- The generated SELECT lists only the columns you named, and aggregates are computed in the database
- A projected DTO is not tracked, so you also avoid the snapshot and identity map cost
- Navigate inside Select rather than adding Include; EF Core adds the join it needs
- Load the entity when the code will change it, use behaviour on it, or needs nearly all of it
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Narrow one real query
Find a list endpoint or page in your own code that returns entities. Write down which fields the caller displays.
Add a record with exactly those fields, project into it, and compare the generated SQL before and after.
Show solution
You should see the column list shrink to what you named, and any Include disappear in favour of the join EF Core adds for the navigation you used.
The reason this is worth doing on real code rather than an example is that the saving is proportional to the columns you were not using. A narrow table barely changes. A table with notes, addresses and audit columns changes a lot.
It also tends to reveal that the endpoint was returning fields nobody consumes, which is a contract problem as much as a performance one.
Think about it
Why memory drops too
A colleague says projection cannot help their query, because the table has only six narrow columns and they need four of them.
Name one thing they still gain, and one situation where they are right to leave it as an entity query.
Show solution
They still avoid change tracking. No snapshot of the loaded values, no identity map entry, and no change detection work on those objects. On a few thousand rows that is measurable, and it costs nothing if the data is read-only.
They are right to keep the entity if the code goes on to modify and save those rows, or if entity behaviour is used. In that case tracking is not overhead, it is the feature.
The general shape of the answer: projection is about what leaves the database and what the context holds on to. Those are two separate savings, and the second one applies even when the first is small.
Saved in this browser only.