Select Like a Surgeon: LINQ Projection That Doesn't Bleed Data
Your entity has 47 columns. Your UI needs 3. You're loading all 47 every single time.
This is the silent performance killer in most .NET applications — and Select is the scalpel that fixes it.
The Problem With Loading Everything
var products = dbContext.Products
.Where(p => p.CategoryId == 5)
.ToList();
// In the view, you only use:
foreach (var p in products)
{
Console.WriteLine($"{p.Name}: {p.Price}");
}
Entity Framework dutifully loads every column: Id, Name, Description, Price, Cost, Stock, SupplierId, CreatedAt, ModifiedAt, ImageBlob, Specifications... the full 47-column monstrosity.
You used two.
The Select Fix
var products = dbContext.Products
.Where(p => p.CategoryId == 5)
.Select(p => new { p.Name, p.Price }) // Only these two columns
.ToList();
Generated SQL:
SELECT Name, Price FROM Products WHERE CategoryId = 5
Less data transferred. Faster query. Lower memory. Same result.
Anonymous Types vs DTOs
For quick projections, anonymous types work great:
var results = query.Select(p => new { p.Name, p.Price, p.Stock });
But you can't return anonymous types from methods. For that, create a DTO:
public record ProductSummary(string Name, decimal Price);
public List<ProductSummary> GetSummaries()
{
return dbContext.Products
.Select(p => new ProductSummary(p.Name, p.Price))
.ToList();
}
Fun fact: The record keyword in C# 9+ generates Equals, GetHashCode, and ToString automatically — perfect for DTOs. Before records, we wrote 30 lines of boilerplate for what's now a single line.
Projection With Computed Values
Select isn't just for cherry-picking columns — you can compute:
var analysis = dbContext.Products
.Select(p => new
{
p.Name,
Profit = p.Price - p.Cost,
InStock = p.Stock > 0,
DisplayPrice = "$" + p.Price.ToString("F2") // Careful!
})
.ToList();
Wait — that DisplayPrice line. Will it run in SQL or C#?
The answer: EF Core will try to translate it. Simple string operations often work. Complex formatting might fail. When in doubt, project the raw value and format after materialization.
Nested Projections
Here's where it gets powerful. Related entities without loading full objects:
var orders = dbContext.Orders
.Select(o => new
{
o.OrderNumber,
CustomerName = o.Customer.Name, // Traverses relationship
Items = o.OrderItems.Select(i => new
{
i.Product.Name,
i.Quantity,
Total = i.Quantity * i.UnitPrice
}).ToList()
})
.ToList();
This generates a single efficient query with JOINs — no N+1 problem, no loading full Customer or Product entities.
The Conditional Projection
Sometimes you need different shapes based on conditions:
var products = dbContext.Products
.Select(p => new
{
p.Name,
p.Price,
Status = p.Stock > 10 ? "Available"
: p.Stock > 0 ? "Low Stock"
: "Out of Stock"
})
.ToList();
This ternary logic translates to SQL CASE WHEN statements.
SelectMany: Flattening Nested Collections
When each source element has a collection, and you want a flat result:
// Each order has multiple items. Get all items across all orders:
var allItems = dbContext.Orders
.SelectMany(o => o.OrderItems) // Flatten
.Select(i => new { i.ProductId, i.Quantity })
.ToList();
SelectMany is the LINQ equivalent of SQL's implicit join that expands rows.
The Rule
- Always project when you don't need all columns
-
Project early in the query chain — before
ToList() - Use DTOs when returning from methods or crossing boundaries
- Nest projections to avoid N+1 and reduce transferred data
- Test complex expressions — not everything translates to SQL
Next time, we'll tackle the GroupBy operator — the one LINQ feature that's simultaneously powerful and confusing. We'll see how it differs between in-memory and database execution, and why your grouping query might not behave as expected. Hope to see you!
Top comments (0)