LINQ in Practice · Lesson 14

IntersectBy and ExceptBy as membership policies

Select entities by whether their keys appear in another collection.

How it works

An import or selection workflow often has a collection of full records and a separate collection of identifiers. IntersectBy returns records whose selected keys belong to the second set. ExceptBy returns records whose keys do not. Neither operation needs a second collection of fabricated Row objects just to perform the membership check.

The sample's selected keys are two and four. The included output contains those rows, and the excluded output contains one and three. Both results receive explicit ordering for display. The query keeps each full Row available, which would matter if the records also contained names, prices, or other information needed later.

These operators have set semantics around keys. Repeated matching records are not a promise to preserve every duplicate row. If the intended operation is merely to filter all source rows, including duplicates, construct a HashSet of keys and use Where with Contains instead. The distinction can change totals in a report.

The comparer is another part of the contract when keys are strings. A list of selected email-like identifiers may require a documented case policy, while an opaque token may be case-sensitive. Supplying a comparer is preferable to relying on a coincidental casing convention in the test data.

Practice by inserting a second Row with identifier two into the source and repeating two in the selected key sequence. Compare IntersectBy with Where against a HashSet. Count the results before printing identifiers and explain which operation removes duplicate identity keys and which operation preserves duplicate source rows.

Then add a selected identifier that has no corresponding source row. Report missing requested keys separately rather than assuming an intersection proves that every request was satisfied. Membership selection answers which source entities qualify; completeness validation answers whether the source contained everything the caller asked for. A reliable workflow often needs both checks.

A working example

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

var rows = new[] { new Row(1), new Row(2), new Row(3), new Row(4) };
int[] selected = { 2, 4 };
var included = rows.IntersectBy(selected, row => row.Id).OrderBy(row => row.Id);
var excluded = rows.ExceptBy(selected, row => row.Id).OrderBy(row => row.Id);
Console.WriteLine(string.Join(",", included.Select(row => row.Id)));
Console.WriteLine(string.Join(",", excluded.Select(row => row.Id)));
record Row(int Id);

Expected output

2,4
1,3
Try this
The second sequence contains keys, while the first contains complete entities.