# EF Core LINQ Query Standards · EF-03

## Standard Metadata

- Area: ORM
- Owner: Backend Steering group
- Last reviewed: 2026-09-09
- Next review due from: 2026-12-01
- Enforcement: CodeAnalysis / Rosyln Analyser / code review
- Status: Active

## Project to DTOs or anonymous types for read-only queries · EF-03.1 · **MUST**

Use `.Select()` to project only the required columns into a Data Transfer Object (DTO) for the callers needs. This is crucial to reduce the queries impact on database performance, application memory and network load.

If a projection is returned across an API boundary, or is reused multiple times, use a DTO. Anonymous types are fine for locally-scoped or one-off projections.

### Good Example: Projecting to a DTO

```cs
var bookSpineDtos = await _context.Books
    .WhereIsPublished()
    .Select(book => new BookSpineDto
    {
        Id = book.Id,
        Title = book.Title,
        AuthorName = book.Author.Name,
        PublisherName = book.Publisher.Name
    })
    .ToListAsync(cancellationToken);
```

### Good Example: Projecting to an anonymous type

```cs
var publishedBookTitles = await _context.Books
    .WhereIsPublished()
    .Select(book => new
    {
        book.Id,
        book.Title
    })
    .ToListAsync(cancellationToken);
```

### Bad Example: Fetching full entities for a read

```cs
// Pulls every column on Books, Authors and Publisher
var bookSpineContents = await _context.Books
    .Include(book => book.Author)
    .Include(book => book.Publisher)
    .WhereIsPublished()
    .ToListAsync(cancellationToken);
```

---

## Don't `Include` unnecessarily · EF-03.2 · **MUST**

Only `.Include()` related entities when they're needed.

Use `Include()` when you are modifying entities via navigation properties without projection.

Including a navigation will load all columns of that table row into an entity, powering navigation properties. This could wastefully load an entire, unused collection of objects into memory.

**IMPORTANT**:
When a query ends in a `.Select()` projection, `.Include()` is unnecessary, EF generates the necessary joins from the navigations referenced in the projection (see EF-03.1 Good Example).

There is 

### Good Example: Include only what's needed

```cs
var order = await _context.Orders
    .Include(order => order.LineItems)
    .FirstOrDefaultAsync(order => order.Id == orderId, cancellationToken);

foreach (var lineItem in order.LineItems) 
{
    lineItem.MarkAsShipped();
}

await _context.SaveChangesAsync(cancellationToken);
```

### Bad Example: Including unused navigations

```cs
// Customer and Invoices are loaded but never read by the caller.
var order = await _context.Orders
    .Include(order => order.LineItems)
    .Include(order => order.Customer) // Customer is unused.
    .FirstOrDefaultAsync(order => order.Id == orderId, cancellationToken);

foreach (var lineItem in order.LineItems)
{
    lineItem.MarkAsShipped();
}

await _context.SaveChangesAsync(cancellationToken);
```

### Bad Example: Redundant includes

```cs
var bookSpineDtos = await _context.Books
    .WhereIsPublished()
    .Include(book => book.Author) // Ignored by EF because the projection's book.Author reference already generated a join
    .Include(book => book.Publisher) // Ignored by EF because the projection's book.Publisher reference already generated a join
    .Select(book => new BookSpineDto
    {
        Id = book.Id,
        Title = book.Title,
        AuthorName = book.Author.Name,
        PublisherName = book.Publisher.Name
    })
    .ToListAsync(cancellationToken);
```

---

## Use async methods for database queries · EF-03.3 · **MUST**

Database access is I/O-bound. Synchronous query methods block the calling thread for the duration of the round trip, which under load exhausts the thread pool and degrades throughput. Always use the async equivalents (`ToListAsync`, `FirstOrDefaultAsync`, `AnyAsync`, `CountAsync`, `SaveChangesAsync`, etc.) and pass a `CancellationToken` where one is available.

### Good Example: Async query

```cs
var book = await _context.Books
    .FirstOrDefaultAsync(book => book.Id == id, cancellationToken);
```

### Bad Example: Sync query

```cs
// Blocks the calling thread for the full database round trip.
var book = _context.Books
    .FirstOrDefault(book => book.Id == id);
```

---

## Use `AsSplitQuery()` when including multiple collection navigations · EF-03.4 · **SHOULD**

Use `.AsSplitQuery()` when a query includes **two or more collection navigations**. EF then issues one SQL query per included collection instead of a single joined query, avoiding the duplication.

Without this, the resulting SQL will create a severe performance issue, resulting in an exponentially multiplied dataset.

### Good Example: Splitting a query with multiple collection includes

```cs
var order = await _context.Orders
    .Include(order => order.LineItems)
    .Include(order => order.Shipments)
    .AsSplitQuery()
    .FirstOrDefaultAsync(order => order.Id == orderId, cancellationToken);
```

### Bad Example: Single query with multiple collection includes

```cs
// LineItems and Shipments are joined together, so each order row is repeated and the result set is (LineItems.Count x Shipments.Count).
var order = await _context.Orders
    .Include(order => order.LineItems)
    .Include(order => order.Shipments)
    .FirstOrDefaultAsync(order => order.Id == orderId, cancellationToken);
```

## Prefer navigation properties over explicit joins · EF-03.5 · **SHOULD**

EF Core can translate navigation properties directly into the appropriate SQL joins. Writing explicit `.Join()` calls duplicate information already expressed by the relationship in the model, is harder to read, and bypasses configured behaviour like global query filters. Use navigation properties unless joining on something that isn't a configured relationship.

### Good Example: Using navigation properties

```cs
var books = await _context.Books
    .Where(book => book.Author.Country == "GB")
    .Select(book => new { book.Title, AuthorName = book.Author.Name })
    .ToListAsync(cancellationToken);
```

### Bad Example: Manual join

```cs
var books = await _context.Books
    .Join(_context.Authors,
        book => book.AuthorId,
        author => author.Id,
        (book, author) => new { Book = book, Author = author })
    .Where(bookAuthor => bookAuthor.Author.Country == "GB")
    .Select(bookAuthor => new { bookAuthor.Book.Title, AuthorName = bookAuthor.Author.Name })
    .ToListAsync(cancellationToken);
```

---
