LINQ's Set Operators
LINQ provides four set-based operators that treat sequences like mathematical sets rather than ordered lists: Union() combines two sequences and removes duplicates, Intersect() returns only elements present in both sequences, Except() returns elements in the first sequence that are absent from the second, and Distinct() removes duplicates from a single sequence. All four use equality comparison (EqualityComparer<T>.Default by default, or a supplied IEqualityComparer<T>) to determine 'sameness', and all four are deferred — they don't run until you enumerate the result, and internally they build a hash-based lookup for efficiency rather than doing nested-loop comparisons.
Cricket analogy: Combining the squad lists from two different IPL franchises a player has represented, removing the player's name if it appears in both, mirrors how Union() merges two sequences while deduplicating shared entries.
Intersect and Except in Practice
Intersect() and Except() are asymmetric in a way that trips people up: skillsA.Intersect(skillsB) returns the same result regardless of argument order (intersection is commutative), but skillsA.Except(skillsB) is not the same as skillsB.Except(skillsA) — Except() returns 'what's in the first sequence but missing from the second', so a.Except(b) finds elements unique to a, while b.Except(a) finds elements unique to b. A classic real use is finding a symmetric difference (elements in exactly one of the two sets) by combining a.Except(b).Union(b.Except(a)), since LINQ has no single built-in operator for that.
Cricket analogy: Comparing a player's IPL-only skills against their Test-only skills gives asymmetric results depending on direction — 'what IPL skills aren't in Tests' differs from 'what Test skills aren't in IPL' — mirroring how a.Except(b) differs from b.Except(a).
Distinct and Custom Equality Comparers
Distinct() removes duplicate elements from a single sequence, and like the other set operators it needs a way to determine equality — for value types and strings the default comparer works naturally, but for custom classes you must either override Equals()/GetHashCode() or pass an IEqualityComparer<T> to get meaningful deduplication instead of accidentally keeping every 'duplicate' because reference equality never matches. Since .NET 6, DistinctBy(keySelector) lets you deduplicate based on just one property (e.g., DistinctBy(p => p.Email)) without needing a full custom comparer, mirroring how UnionBy, IntersectBy, and ExceptBy work for the other set operators.
Cricket analogy: Deduplicating a list of player appearances by matching jersey number and team rather than by object reference ensures two DTOs representing the same player are recognized as duplicates, mirroring why Distinct() needs a proper Equals() override on custom classes.
var teamA = new List<string> { "Alice", "Bob", "Carol" };
var teamB = new List<string> { "Bob", "Dave", "Eve" };
var combined = teamA.Union(teamB); // Alice, Bob, Carol, Dave, Eve
var shared = teamA.Intersect(teamB); // Bob
var onlyInA = teamA.Except(teamB); // Alice, Carol
var onlyInB = teamB.Except(teamA); // Dave, Eve — order matters!
// Symmetric difference: elements unique to exactly one side
var symmetricDifference = onlyInA.Union(onlyInB); // Alice, Carol, Dave, Eve
public record Customer(int Id, string Email);
var customers = new List<Customer>
{
new(1, "a@example.com"),
new(2, "a@example.com"), // duplicate email, different Id
new(3, "c@example.com"),
};
// DistinctBy (since .NET 6) dedupes by a single key without a full comparer
var uniqueByEmail = customers.DistinctBy(c => c.Email); // 2 customers remainExcept() and Intersect() are order-sensitive on the receiver: a.Except(b) is not the same as b.Except(a). Only Union() and Intersect() are commutative in the mathematical sense of returning equivalent element sets — Except() explicitly means 'first sequence minus second'.
All four set operators build an internal hash set from the second sequence (or from previously-seen elements for Distinct()) to achieve roughly O(n + m) performance rather than the O(n × m) you'd get from a naive nested-loop comparison — this is why a well-defined GetHashCode() matters for custom types.
- Union() merges two sequences and removes duplicates; Intersect() keeps only elements present in both; Except() keeps elements in the first sequence but not the second.
- Distinct() removes duplicates within a single sequence.
- Except() and the ordering of arguments matters — a.Except(b) differs from b.Except(a); Union() and Intersect() are commutative.
- All four rely on equality comparison — override Equals()/GetHashCode() or pass IEqualityComparer<T> for custom types.
- DistinctBy, UnionBy, IntersectBy, and ExceptBy (since .NET 6) allow key-based comparison without a full custom comparer.
- Internally these operators use hash-based lookups for roughly O(n + m) performance.
- Symmetric difference (elements unique to exactly one sequence) requires combining Except() calls with Union().
Practice what you learned
1. Is a.Except(b) the same as b.Except(a)?
2. What is required for Distinct() to correctly deduplicate a sequence of custom reference-type objects?
3. Which two of LINQ's set operators are commutative (argument order doesn't change the result)?
4. What does DistinctBy(c => c.Email), introduced in .NET 6, let you do?
5. How would you compute the symmetric difference (elements unique to exactly one of two sequences) in LINQ?
Was this page helpful?
You May Also Like
Any, All, and Contains
Master LINQ's boolean-returning quantifier operators — Any, All, and Contains — and how their short-circuiting behavior makes them efficient existence checks.
Aggregate and Custom Accumulators
Learn how LINQ's Aggregate() operator implements a general-purpose fold, letting you build custom accumulation logic beyond Sum, Count, and Average.
First, Single, and ElementAt
Understand the differences between First, Single, and ElementAt for extracting one element from a LINQ sequence, and how their exception behavior differs.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics