LINQ in Practice · Lesson 9

Joining related rows

Match records by a key rather than by position.

Join records through keys instead of positions

The orders and customer arrays are arranged differently, so pairing them by array index would connect the wrong people to amounts. Join associates records whose selected keys compare equal. The outer order key is CustomerId and the inner customer key is Id. A key relationship lets each dataset retain its own order while the query constructs a combined report. The important contract is not that the arrays have equal lengths, but that the chosen identifiers represent the relationship the domain intends.

Follow the outer order sequence

The first outer order belongs to customer two and has total twelve. Customer two is Lin, so the first projected string is Lin:12. The second order belongs to customer one, whose name is Ada, and produces Ada:7. Joining those strings prints Lin:12,Ada:7. The customer array starts with Ada, but that does not reorder the outer orders. This in-memory Join preserves the outer sequence’s contribution order while finding matching inner records for each outer element.

orders.Join(customers, o => o.CustomerId, c => c.Id, (o, c) => $"{c.Name}:{o.Total}")

The projection runs for matching pairs

The final lambda receives one matched order and one matched customer. It is not a mutation step and does not rewrite either array; it creates the report value for that pair. Thinking in terms of pairs explains cardinality: if an inner key occurs twice, one order can produce two report rows. A relational-looking query does not automatically enforce unique customer identifiers. Source validation must establish that uniqueness when the application requires one customer record per identifier.

An inner join drops unmatched outer rows

Join is an inner join. An order with a CustomerId absent from the customer array contributes no result, rather than a row labeled missing. That may be correct for an intersection report and incorrect for an audit that must account for every order. A successful formatted result cannot prove that every outer record was matched. Compare accepted counts or report missing keys separately, and use an explicitly designed left-join shape when preserving unmatched orders is part of the requirement.

Practice: break positional assumptions

Reverse the customer array while keeping its identifiers and names unchanged. Predict that the baseline report remains Lin:12,Ada:7 because matching uses keys, not positions. Then reverse only the order array and predict that the report order becomes Ada:7,Lin:12. These independent changes make the two sequence roles visible. Finally, add an order for a nonexistent identifier and record why the inner-join result does not grow, even though the source order count does.

  • For each outer order, write its selected key and every matching inner key before projecting a string.
  • Keep missing-key validation distinct from the formatting of records that successfully matched.

Practice: expose duplicate-key multiplication

In a separate experiment, add a second customer row with identifier two and a different display name. Predict the two report pairs created for the first order. Then write a validation check that rejects duplicate customer identifiers before joining, if that is the domain’s intended rule. The point is not to hide multiplication by selecting an arbitrary customer afterward, but to decide whether repeated inner keys are a valid one-to-many relationship or corrupted source data.

  • Compare source order count, matched order count, and final pair count as three different quantities.
  • If identifiers become strings, choose the equality comparer required by their actual contract.

Name both the relationship and the completeness policy

A clear join explanation identifies outer and inner sequences, key selectors, equality rules, projection, and expected cardinality. It also states what happens when no match or several matches exist. The sample is a deterministic in-memory calculation using ordinary managed LINQ, not a claim about a database query plan or network service. When a report must account for every source row, add that requirement explicitly and test missing and duplicate relationships rather than only the convenient case where every identifier happens to match exactly once.

  • Baseline output is Lin:12,Ada:7, following the outer orders.
  • Joining by key is different from positional Zip pairing.
  • Missing matches can remove rows, and duplicate inner keys can create several pairs per order.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var orders = new[] { new Order(2, 12m), new Order(1, 7m) };
var customers = new[] { new Customer(1, "Ada"), new Customer(2, "Lin") };
var report = orders.Join(customers, o => o.CustomerId, c => c.Id, (o, c) => $"{c.Name}:{o.Total}");
Console.WriteLine(string.Join(",", report));
record Order(int CustomerId, decimal Total);
record Customer(int Id, string Name);

Expected output

Lin:12,Ada:7
Try this
An inner join omits rows that have no matching key.

Further reading