Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Query

Include (pre-loading relations)

<Entity>.Include(o => o.Lines) · .Include(o => o.Lines.Product) · (repeatable)

Pre-load the related rows a query's results are about to navigate to. `Include(o => o.Lines)` does not change what comes back — the same rows, the same type — it just means the children are already in hand, so the loop that walks them costs nothing instead of firing one query per parent. It is the fix for the N+1 problem, and the only reason you ever need to think about it.

stable4 examples compiled by CIquerydatarelationsperformance

Summary#

Include pre-loads the relations your results are about to walk:

entity Order {
  [Required, MaxLength(40)] string Code;
  [ForeignKey(Order)] Line[] Lines;
}

entity Line {
  [Required] Order Order;
  Product Product;                 // the reference the nested Include walks to
  [MaxLength(60)] string Sku;
  decimal Amount;
}

decimal TotalOfRecentOrders() {
  var orders = Order.OrderByDescending(o => o.Code)
                    .Take(50)
                    .Include(o => o.Lines)      // ← the lines come back with the orders
                    .ToList();

  decimal total = 0m;
  foreach (var o in orders) {
    foreach (var l in o.Lines) { total = total + l.Amount; }   // already in memory — no query in this loop
  }
  return total;
}

Delete the Include and that code still works — and quietly fires one query per order. That is the whole point of the verb: in a function body it is a performance declaration, not a semantic one.

On a page it is REQUIRED, not an optimisation. The sentence above is true of code that runs on the server, where a relation you did not pre-load is fetched on demand. A page renders on the client, which has no such fallback: it can only walk what the query actually brought back. See [[query-include#on-a-page]] before you leave one out.

Signature#

<Query>.Include(o => o.Collection)          // a child collection
<Query>.Include(o => o.Reference)           // an entity reference
<Query>.Include(o => o.Lines.Product)       // NESTED — a dotted path in ONE lambda
<Query>.Include(a).Include(b)               // repeatable — they accumulate

A member-selector lambda, not a string. There is no ThenInclude: nesting is a dotted path inside the one lambda, and every step of the path is loaded, not just the leaf.

Description#

It changes performance, not results#

An Include returns exactly the same rows, of exactly the same type, as the query without it. It adds nothing to the SELECT, filters nothing, and never appears in the row shape. All it does is load the related rows into memory with the parents, so the navigation you were going to do anyway is already paid for.

This is worth internalising, because it explains why you can add or remove one freely in a function body: an Include there cannot change what your code computes — only how many round trips it takes to compute it. If adding one changes an answer, the answer was wrong before.

That freedom is the server's, and only the server's. The next section is the other half.

On a page, it IS semantic — and leaving it out is an error#

A page's render runs on the client against the rows the query returned. There is no lazy load out there: a relation that was not included did not travel, so the reference holds its raw key rather than the row it names. Walking it does not fetch anything and does not return empty — it fails, because you asked a key for a property only a row has.

// the query behind the page
var item = Item.Where(i => i.Code == code)
               .Include(i => i.Owner)              // ← REQUIRED: the render reads Owner.Name
               .Include(i => i.Links.Target)       // ← REQUIRED: and it walks THROUGH Links to each Target
               .FirstOrDefault();

render {
  Text(item.Owner.Name);                            // without the first Include: no row, no Name
  foreach (var l in item.Links) { Text(l.Target.Code); }   // without the SECOND: the links came, their targets did not
}

Two things follow, and both cost people time:

A nested walk needs the nested path. Including the collection is not enough — Include(i => i.Links) brings the links and stops there, so l.Target.Code still has nothing to read. The dotted form loads every step.

Do not conclude anything from a page that works without one. Whether an un-included reference resolves depends on whether some other query on the same page already loaded that row, because they share one store. So the same render can be correct on a page that happens to list Owners elsewhere and fail on a page that does not — identical code, different neighbours. Include what you walk, and the question never arises.

The N+1 problem, which is the reason it exists#

Fetch 50 orders, then loop over each one's lines. Without Include, each o.Lines is a fresh query — 1 query for the orders and 50 for the lines. It is fast on your machine with 3 orders and it is an outage on a real database with 5,000. Nothing about the code looks wrong, which is what makes it worth a verb of its own.

With the Include, the children arrive with the parents, and the loop touches memory.

How do I load two levels down?#

A dotted path in one lambda walks further down, loading every step:

entity Product {
  [Required, MaxLength(60)] string Name;
  decimal Price;
}

List<Order> WithEverything() {
  return Order.Include(o => o.Lines.Product)     // loads Lines AND each Line's Product
              .ToList();
}

There is no ThenInclude to chain — the path is the nesting. To load two different branches, call Include twice.

Where it may appear#

Include composes with Where, OrderBy, Skip/Take, ToList() and the single-row terminals (an Include on a First() is perfectly sensible — one parent, its children in hand).

It is refused in three places, each for the same reason — there would be no entity rows for it to attach the relations to:

  • before a Select projection — a projection does not return entity rows. Select what you need.
  • before a GroupBy — a group is not a row.
  • with the set operators — apply it after materialising.

On a list you already hold#

A local list takes Include too, and for exactly the same reason a server query does: holding the ROWS is not the same as holding what they REFER to. A row carries a reference as a value, so a client-side Where that navigates one has nothing to read through unless it was included first.

entity Supplier { [Required] [MaxLength(80)] string Name; security { allow read, create when IsAuthenticated || IsAnonymous; } }
entity Part {
  [Required] [MaxLength(80)] string Code;
  [Required] Supplier Supplier;
  security { allow read, create when IsAuthenticated || IsAnonymous; }
}

int AcmeParts() {
  var parts = Part.ToList();
  return parts.Include(p => p.Supplier).Where(p => p.Supplier.Name == "Acme").Count();
}

The load is batched: one pass per hop, over the distinct references in the whole list — not one read per row. A deeper path (p => p.Supplier.Region) loads the second hop across everything the first hop returned, so the cost does not grow with the number of rows.

Without the Include, navigating p.Supplier.Name in that filter is a compile error that names the fix — it does not silently return blanks.

In a component, it is added for you#

A component's query fetches what its render reads. If the render navigates a reference the query did not include, the compiler adds the Include rather than refusing the code:

live var reports = Report.ToList();      // + .Include(Owner) — added, because the render below reads it

render {
  foreach (var r in reports) { Text(r.Owner.Email); }
}

Reading r.Owner.Email is the request for Owner — there is no program that wants the read and not the fetch — so requiring a second statement of the same fact only creates something that can drift out of step with the first. The two spellings behave identically: a reference navigated inside a client-side Where is the same demand as one read in an element, and both are added.

This is also how you end up on the efficient path without having to know about it. What gets added is an eager load — one query with a join, batched per hop as described above — so the default is the one that avoids [[#n-plus-one|N+1]], not a fetch per row.

It is not invisible: the editor shows what was added as a hint after the query (+ .Include(Owner)), so what the query costs is still readable at the point you are reading it.

The refusal is still there when the compiler cannot satisfy the demand — a read through a reference on something that is not a component query field is still a compile error naming the fix. Dropping such a read silently is the one outcome worse than refusing it: the page would render a blank where the value should be.

Examples#

Include on a single-row terminal — the shape a detail page uses:

Order? Detail(string code) {
  return Order.Where(o => o.Code == code)
              .Include(o => o.Lines.Product)
              .FirstOrDefault();
}

See also#

Related

Querying data

How you read data in Osy#. You write C# LINQ; it becomes one SQL statement. The rules that follow from that are the…

Child collections (navigating a relation)

A parent's child collection — `order.Lines` — is not a loaded array. It is a QUERY, correlated to that parent, and…

relations

One entity points at another by declaring it as a member — that is the foreign key. The parent reads its children back…

Select (projections)

Reshape what a query returns: one column, an anonymous row, or a `class` you declared. The projection becomes the SQL…