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

Reference / Query

Join / LeftJoin / SelectMany

a.Join(B, x => x.Key, y => y.Key, (x, y) => new T { … }) · a.LeftJoin(…) · a.SelectMany(x => x.Children, (x, c) => new T { … })

Combine two entities into one result. `Join` keeps the rows that match on both sides; `LeftJoin` keeps every row on the left and gives you null on the right where there is no match; `SelectMany` flattens a parent and its children into one row per child. Most of the time you do NOT need any of them — a relation you declared is navigated, not joined — so reach for these when the two things are related by a VALUE rather than by a reference.

stable3 examples compiled by CIquerydatajoins

Summary#

Combine two entities into one row shape:

class Row {
  public string OrderCode;
  public string CustomerName;
}

entity Customer {
  [Required, MaxLength(60)] string Ref;
  [MaxLength(120)] string Name;
}

entity Order {
  [Required, MaxLength(40)] string Code;
  [MaxLength(60)] string CustomerRef;      // related by a VALUE, not by a reference
  [ForeignKey(Order)] Line[] Lines;
}

List<Row> Rows() {
  return Order.Join(Customer,
                    o => o.CustomerRef,     // the key on the left
                    c => c.Ref,             // the key on the right
                    (o, c) => new Row { OrderCode = o.Code, CustomerName = c.Name })
            .ToList();
}

Signature#

<Entity>.Join(<Other>, a => aKey, b => bKey, (a, b) => projection)       // INNER JOIN — matches on both sides
<Entity>.LeftJoin(<Other>, a => aKey, b => bKey, (a, b) => projection)   // LEFT JOIN — every left row; null on the right
<Entity>.SelectMany(a => a.Children, (a, c) => projection)               // flatten: one row per child
<Entity>.SelectMany(a => <Other>, (a, b) => projection)                  // every pairing (a cross join)

The range-variable names must be the same in the key selectors and the result selector — o and c above. That is not a style rule; the compiler binds them by name.

Description#

First: you usually do not need a join#

This is the most useful thing on the page. If the two entities are related by a declared relation, you do not join them — you navigate:

order.Customer.Name                          // a reference: just read it
order.Lines.Sum(l => l.Amount)               // a collection: query it ([Child collections (navigating a relation)](/reference/query/collections/))
Order.Include(o => o.Lines.Product).ToList() // pre-load a whole graph ([Include (pre-loading relations)](/reference/query/include/))

The relation already knows how the rows connect. Writing the join condition again by hand is a re-derivation of a fact the model holds — and a chance to get it wrong.

Reach for a join when there is no relation to navigate: two entities related by a shared value (a code, a reference, a slug) rather than by a foreign key. That is what the example above is — CustomerRef is a string that happens to match Customer.Ref, and no relation ties them.

Join vs LeftJoin#

  • Join keeps only the rows that match on both sides. An order whose CustomerRef matches no customer simply is not in the result — which is sometimes exactly right, and sometimes how a row silently disappears from a report.
  • LeftJoin keeps every row on the left, and hands you null on the right where nothing matched. Reach for it when the left side is the thing you are reporting on and the right side is extra detail.
class Report {
  public string OrderCode;
  public string? CustomerName;   // null when nothing matched — that is the point
}

List<Report> AllOrders() {
  return Order.LeftJoin(Customer,
                        o => o.CustomerRef,
                        c => c.Ref,
                        (o, c) => new Report { OrderCode = o.Code, CustomerName = c.Name })
              .ToList();
}

If a report is missing rows you know exist, an inner join is the first thing to suspect.

How do I get one row per child? — SelectMany#

SelectMany turns a parent and its children into one row per child, which is the shape a flat export or a line -level report wants:

class LineRow {
  public string OrderCode;
  public string Sku;
  public decimal Amount;
}

entity Line {
  [Required] Order Order;
  [MaxLength(60)] string Sku;
  decimal Amount;
}

List<LineRow> Flat() {
  return Order.SelectMany(o => o.Lines,
                          (o, l) => new LineRow { OrderCode = o.Code, Sku = l.Sku, Amount = l.Amount })
               .ToList();
}

Note what it is not: this does not fetch orders and then their lines. It is one statement — a join on the child's foreign key — returning line-shaped rows.

Given an unrelated entity instead of a collection, SelectMany produces every pairing of the two (a cross join). That is occasionally what you want and much more often a mistake; be sure.

What does a join hand back?#

Each of these takes a result selector, so a join always ends in a shape you named — usually a class. There is no "joined entity" type to hand back: you say what the combined row looks like, and that is what you get. A trailing Where / OrderBy / Take / Skip may follow, and the chain must end in that projection.

These are entity-only. There is no join over a local list yet.

Examples#

See the fences above — an inner join on a value, the left join that keeps the unmatched rows, and SelectMany for a line-level flatten.

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…

Include (pre-loading relations)

Pre-load the related rows a query's results are about to navigate to. `Include(o => o.Lines)` does not change what…

Select (projections)

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

relations

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