Query Strategy and Loading Profiles
Control how the repository executes a query — from full entity tracking to raw projections.
[QueryStrategy] — Loading Strategy
Section titled “[QueryStrategy] — Loading Strategy”The Problem
Section titled “The Problem”Different operations need different loading strategies. A mutation needs a tracked entity with all navigations loaded. A list query needs a lightweight projection with no tracking. An admin dashboard needs raw data bypassing all filters. A grid endpoint needs filtered data with specific includes.
Without explicit control, every query loads entities the same way — usually tracked with all filters applied. This wastes resources for read-only scenarios and prevents admin access to filtered-out data.
The Solution
Section titled “The Solution”[QueryStrategy(Strategy = QueryStrategy.Projection)][Query<Order, OrderListDto>]public partial class GetOrderList{ // Loaded as projection — no entity tracking, best performance}
[QueryStrategy(Strategy = QueryStrategy.Entity)][Mutation(Mode = MutationMode.Update)]public partial class UpdateOrder{ // Loaded as tracked entity — full change tracking}QueryStrategy Options
Section titled “QueryStrategy Options”| Strategy | Tracking | Filters | Use Case |
|---|---|---|---|
Projection | No | Applied | Read-only lists, DTOs, API responses |
Entity | Yes | Applied | Mutations, updates, business logic |
Filtered | Yes | Applied + custom FilterMap | Queries needing navigation-level filtering |
Raw | No | None | Admin dashboards, data exports, migrations |
Default behavior: If you don’t specify [QueryStrategy], the source generator infers the strategy from the operation type:
- Mutations →
Entity(needs tracking forSaveChanges) - Queries with a projection type →
Projection(best performance) - Queries without projection →
Entity
When to Override
Section titled “When to Override”// Admin endpoint that needs to see soft-deleted records[QueryStrategy(Strategy = QueryStrategy.Raw)][Query<Order, OrderAdminDto>]public partial class GetAllOrdersAdmin { }
// Read query that needs custom navigation filtering[QueryStrategy(Strategy = QueryStrategy.Filtered)][Query<Order, OrderDetailDto>]public partial class GetOrderWithFilteredLines { }[LoadWith<T>] — Loading Profiles
Section titled “[LoadWith<T>] — Loading Profiles”The Problem
Section titled “The Problem”Different views of the same entity need different navigation loading. An order list needs just the order fields. An order detail needs Customer and LineItems. An order invoice needs LineItems.Product. Writing Include() chains everywhere is repetitive, easy to get wrong, and invisible to the source generator.
The Solution
Section titled “The Solution”Declare what you need on the query class. The source generator reads the entity’s [Relation.*] attributes and generates the correct Include() / ThenInclude() calls.
[LoadWith<Order>(MaxDepth = 2, SplitQuery = true)][Query<Order, OrderDetailDto>]public partial class GetOrderDetail{ [Filter] public Guid? OrderId { get; set; }}Properties
Section titled “Properties”| Property | Type | Default | Description |
|---|---|---|---|
MaxDepth | int | 1 | How deep to include navigations (0 = none) |
SplitQuery | bool | false | Use split queries for collections (avoids cartesian explosion) |
How Depth Works
Section titled “How Depth Works”Given an Order entity with navigations to Customer, LineItems, and LineItems.Product:
Depth 0: Order only (no navigations)Depth 1: Order -> Customer, Order -> LineItemsDepth 2: Order -> Customer -> Address, Order -> LineItems -> ProductDepth 3: Order -> LineItems -> Product -> CategoryThe source generator reads all [Relation.*] attributes on the entity and its related types, then generates Include() / ThenInclude() calls up to MaxDepth.
Performance warning: Depth > 3 emits diagnostic PRAG0711. Deep includes cause large SQL joins and cartesian explosion with collection navigations. If you need data from deeply nested types, consider using QueryStrategy.Projection to select only the fields you need.
When to Use SplitQuery
Section titled “When to Use SplitQuery”EF Core joins all navigations into a single SQL query by default. With collection navigations, this causes cartesian explosion — every row in the parent is repeated for every row in each child collection.
// Without SplitQuery: one big JOIN// If Order has 3 LineItems and 2 Tags: 3 x 2 = 6 rows returned for one Order[LoadWith<Order>(MaxDepth = 2)]
// With SplitQuery: separate SQL queries per collection// Query 1: Orders + Customer (1:1 join, no duplication)// Query 2: LineItems for matched Orders// Query 3: Tags for matched Orders[LoadWith<Order>(MaxDepth = 2, SplitQuery = true)]Rule of thumb: Use SplitQuery = true when including more than one collection navigation at the same depth level.
How QueryStrategy, LoadWith, and Queries Compose
Section titled “How QueryStrategy, LoadWith, and Queries Compose”The attributes control orthogonal concerns:
[QueryStrategy] -> Controls tracking + filter behavior[LoadWith] -> Controls eager loading (Include depth + split)[Query] -> Controls filtering + sorting + projectionThey compose in a fixed order during query execution:
- Strategy —
AsNoTracking()applied (if Projection or Raw) - Filters — Query filters applied or skipped (based on strategy)
- Includes —
Include()/ThenInclude()calls generated from[LoadWith] - Where — Filter properties from the query class
- OrderBy — Sort properties from the query class
- Projection —
Select()to the result DTO type - Paging —
Skip()/Take()if pagination properties exist
Full Example
Section titled “Full Example”[QueryStrategy(Strategy = QueryStrategy.Projection)][LoadWith<Order>(MaxDepth = 1)][Query<Order, OrderSummaryDto>]public partial class GetOrderSummaries{ [Filter] public OrderStatus? Status { get; set; }
[Sort(DefaultDirection = SortDirection.Descending)] public SortDirection? CreatedAt { get; set; }
public int Page { get; set; } = 1; public int PageSize { get; set; } = 50;}The generated code:
AsNoTracking()— Projection strategy, no tracking overheadInclude(o => o.Customer),Include(o => o.LineItems)— MaxDepth 1.Where(o => o.Status == status)— only whenStatusis not null.OrderByDescending(o => o.CreatedAt)— default descending.Select(o => new OrderSummaryDto { ... })— projection.Skip((page - 1) * pageSize).Take(pageSize)— pagination
Without Any Attributes
Section titled “Without Any Attributes”If you don’t use [QueryStrategy] or [LoadWith], the defaults apply:
// Minimal query — defaults to Projection strategy, depth 1, no split[Query<Order, OrderDto>]public partial class GetOrders { }
// Minimal mutation — defaults to Entity strategy[Mutation(Mode = MutationMode.Update)]public partial class UpdateOrder { }The source generator picks sensible defaults so you only need attributes when the defaults don’t fit.