Why does ordering with Linq-to-Objects compare items to themselves?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
When you use OrderBy in LINQ-to-Objects and provide a custom IComparer<T>, you may notice that the comparer is called with the same item on both sides — Compare(x, x) returns 0. This is not a bug. It happens because LINQ's internal sorting algorithm (an introspective sort variant) naturally compares elements with themselves as part of its partitioning and pivot selection logic. Understanding this behavior helps you write correct custom comparers and debug unexpected sorting results.
Observing Self-Comparisons
The Compare(3, 3) calls happen during the sorting algorithm's pivot comparisons and boundary checks.
Why the Algorithm Does This
LINQ-to-Objects uses a variant of quicksort internally. The .NET runtime implementation in EnumerableSorter<TElement> uses a quicksort that compares the pivot element against all elements in the partition — including itself:
When the scan indices reach the pivot element, the algorithm compares the pivot to itself. Eliminating this comparison would require an extra conditional check on every iteration, which costs more than the occasional redundant comparison.
Writing a Correct Custom Comparer
A well-behaved comparer must handle self-comparison correctly by returning 0:
When Self-Comparison Causes Problems
If your comparer violates the reflexivity rule (Compare(x, x) must return 0), LINQ may produce incorrect or unstable results:
Using Comparison Delegate Instead
For simpler cases, use the Comparison<T> overload to avoid creating a full comparer class:
Using key selectors with OrderBy/ThenBy is usually preferable to writing custom comparers.
Common Pitfalls
- Comparers that do not return 0 for equal elements: The sorting contract requires that
Compare(x, x) == 0(reflexivity),Compare(x, y) == -Compare(y, x)(antisymmetry), and transitivity. Violating these produces undefined sorting behavior — elements may appear in any order, or the sort may loop indefinitely. - Using mutable state in comparers: A comparer that changes its behavior based on internal state (call count, random values) breaks the sorting contract. Comparers must be pure functions of their two arguments.
- Assuming a specific number of comparisons: The exact number and order of comparisons depend on the sorting algorithm and the input data. Do not write code that relies on a specific comparison sequence — it may change between .NET versions.
- Null reference exceptions in custom comparers: If your collection may contain null elements, your comparer must handle nulls explicitly.
Comparer<T>.Defaulthandles nulls by sorting them first, but custom comparers do not automatically. - Expecting LINQ sort to be stable without
ThenBy: LINQ'sOrderByis documented as a stable sort (equal elements preserve their original order), but this relies on the comparer correctly identifying equal elements. IfCompare(x, y)returns non-zero for logically equal elements, stability is effectively lost.
Summary
- LINQ-to-Objects' sorting algorithm compares items to themselves as a natural consequence of its quicksort-based partitioning
- This is not a bug — it would be more expensive to check for and skip self-comparisons than to perform them
- Custom comparers must be reflexive (
Compare(x, x) == 0), antisymmetric, and transitive - Prefer key selectors (
OrderBy(p => p.Age)) over customIComparer<T>implementations when possible - LINQ's
OrderByis a stable sort — equal elements maintain their original relative order - Never use mutable state or side effects inside a comparer

