DEV Community

Nick
Nick

Posted on AI-assisted

Select Like a Surgeon: LINQ Projection That Doesn't Bleed Data

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}");
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Generated SQL:

SELECT Name, Price FROM Products WHERE CategoryId = 5
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

SelectMany is the LINQ equivalent of SQL's implicit join that expands rows.

The Rule

  1. Always project when you don't need all columns
  2. Project early in the query chain — before ToList()
  3. Use DTOs when returning from methods or crossing boundaries
  4. Nest projections to avoid N+1 and reduce transferred data
  5. 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)