LINQ in Practice · Lesson 5

Ordering and tie breakers

Make repeated report output deterministic.

A report needs a complete ordering policy

Sorting by one field is not always enough to produce the order a product expects. Several records can share the same price, so a secondary rule makes their relative presentation explicit. The sample treats price as the primary ordering key and product code as an ordinal tie breaker. Primary order and tie order answer different questions. A complete policy lets a reader predict the report even when the incoming records were assembled in a different incidental order.

Follow the lowest price before comparing codes

The incoming products are B at ten, A at ten, and C at five. OrderBy places C first because five is the lowest price. A and B still tie on the primary key, so ThenBy compares their codes using StringComparer.Ordinal and places A before B. Projecting only the codes and joining them produces C,A,B. The result is not a global alphabetical sort: C remains first even though A and B precede it alphabetically, because price retains higher priority.

products.OrderBy(x => x.Price).ThenBy(x => x.Code, StringComparer.Ordinal)

Secondary ordering refines an existing order

ThenBy extends the ordering established by OrderBy. It resolves comparisons for elements equal under the earlier key rather than replacing the primary rule. Adding another OrderBy instead would establish another primary ordering operation, which can obscure or change the intended hierarchy. Write the chain in business-priority order and name each key’s meaning. For identifier-like codes, an explicit ordinal comparer also separates deterministic identifier ordering from culture-aware human-language sorting, which is a different requirement.

Stable sorting is not a substitute for an intended tie rule

LINQ-to-Objects ordering is stable, so elements that remain equal under the chosen keys retain their relative input order. That can be useful, but it does not make arbitrary incoming order a suitable product policy. If a report must be reproducible across different source arrangements, provide enough keys to distinguish the relevant records or define how true ties should be presented. Conversely, adding an identifier tie breaker is not correct if the required behavior is intentionally to preserve arrival order among equal prices.

Practice: alter the hierarchy deliberately

Change only ThenBy to ThenByDescending and predict C,B,A. Then restore it and add a product D priced below C; explain why D becomes first despite its code. Finally, use a second OrderBy on code in a separate experiment and compare the result with the original price-first chain. These variations show exactly which part of the hierarchy changed instead of relying on the vague observation that the collection is still somehow sorted.

  • Write the complete ordered list of price-code pairs before projecting away the prices.
  • Change one ordering decision per experiment and preserve the baseline C,A,B for comparison.

Practice: define the remaining equality case

As an independent exercise, add two products with both the same price and the same code but a separate arrival number. Decide whether arrivals should retain input order or whether the arrival number should become another explicit key. Then reverse those two source rows and check the chosen policy. The exercise forces a clear distinction between a stable implementation rule and a domain-level definition of which records should be considered tied for presentation.

  • If codes are human-facing words rather than identifiers, document the intended comparer instead of assuming ordinal order is always appropriate.
  • Test repeated keys and changed source order, not only an input where every primary key is different.

Keep ordering independent from equality and identity

Sorting decides presentation or processing sequence; it does not remove duplicate records or establish business identity. Two products can compare equally under this ordering while still being distinct records. When using sorted output for paging, reproducible exports, or tests, state every ordering requirement and preserve any meaningful source-order rule. The sample uses small in-memory data and an explicit code comparer supported by the browser runtime. It does not require a locale-specific collation service or justify assumptions about a remote database’s ordering behavior.

  • Baseline output is C,A,B, with price taking precedence over code.
  • ThenBy refines the earlier order; a new OrderBy is another primary ordering decision.
  • A deterministic tie policy must match the domain, not merely make one test convenient.

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 products = new[] { new Product("B", 10m), new Product("A", 10m), new Product("C", 5m) };
Console.WriteLine(string.Join(",", products.OrderBy(x => x.Price).ThenBy(x => x.Code, StringComparer.Ordinal).Select(x => x.Code)));
record Product(string Code, decimal Price);

Expected output

C,A,B
Try this
ThenBy extends an ordering; another OrderBy replaces the primary key.

Further reading