1. Introduction
Let's jump into a super interesting and practical topic: LINQ set operations — Union, Intersect, Except. These let you work with collections like you're doing real set algebra (or counting stickers in two packs on a programmer's desk — which, honestly, is pretty much the same thing). If you've never heard of set algebra (or just forgot about it), it might sound scary. But it's actually a really simple concept: imagine two bags of fruit. Set algebra is just a way to figure out which fruits are in both bags, which are only in one, and what you get if you combine them. It's like playing with stickers: you can put everything together, find the ones that match, or remove some from others. That's it.
Practice: These methods are super handy when you need to combine results from two different queries, find common elements, or figure out the difference between collections. For example:
- Make a full list of all products that show up either in the sales list or the purchases list.
- Find products that are in both lists — the common items.
- Or figure out which products are in the warehouse but haven't been bought — someone's definitely gathering dust!
Real-world tasks: Set algebra pops up everywhere: filtering users by subscriptions, finding people who were in different groups, searching for unique or overlapping orders, comparing results from two different database queries, and tons more.
2. The Union Operation — Merging Collections
It's simple, so let's get straight to the code. Let's say we have two product lists:
List<string> warehouseProducts = new List<string> { "Milk", "Bread", "Cheese", "Eggs" };
List<string> recentlySold = new List<string> { "Bread", "Cheese", "Salami", "Tea" };
What does Union do?
Union returns unique elements that are in at least one of the collections.
Basically, it's a merge.
var allProducts = warehouseProducts.Union(recentlySold);
foreach (var product in allProducts)
{
Console.WriteLine(product);
}
// Output: Milk, Bread, Cheese, Eggs, Salami, Tea
Here's what it looks like visually:
| Warehouse | Sold | Union (Merged) |
|---|---|---|
| Milk | Bread | Milk |
| Bread | Cheese | Bread |
| Cheese | Salami | Cheese |
| Eggs | Tea | Eggs |
| Salami | ||
| Tea |
In set algebra terms, Union is the "or" operation: give me everything that's anywhere. Imagine two boxes with different kinds of tea — if you decide to try all the flavors, it doesn't matter if some teas repeat — you'll only drink each one once.
Union automatically removes duplicates (based on how Equals and GetHashCode are implemented for the element type). If you're merging your own classes (like Product), make sure those methods are implemented right, or Union will act weird: the same elements might be treated as different!
Don't forget: The order of elements in the result keeps the order from the first collection, and new ones get added at the end (in the order they first show up in the second collection).
Example with objects
Let's keep building our "Store" app. We have two Product lists:
public class Product
{
public string Name { get; set; }
public string Category { get; set; }
// For Union/Intersect/Except to work right, you need proper Equals and GetHashCode!
public override bool Equals(object? obj) =>
obj is Product other && Name == other.Name && Category == other.Category;
public override int GetHashCode() => HashCode.Combine(Name, Category);
}
List<Product> stock = new()
{
new Product { Name = "Milk", Category = "Dairy" },
new Product { Name = "Bread", Category = "Bakery" },
};
List<Product> sold = new()
{
new Product { Name = "Bread", Category = "Bakery" },
new Product { Name = "Salami", Category = "Sausages" },
};
var all = stock.Union(sold);
// Notice: elements aren't duplicated,
// even if they "look" the same, as long as they're logically equal.
foreach (var p in all)
Console.WriteLine($"{p.Name} ({p.Category})");
Classic mistake: If you don't override Equals/GetHashCode, Union will treat different instances with the same values as different elements!
3. The Intersect Operation — Intersecting Collections
Intersect returns the elements that are in all the original collections. It's basically "and" in set algebra.
Example
Let's remember the lists from before:
var commonProducts = warehouseProducts.Intersect(recentlySold);
foreach (var product in commonProducts)
{
Console.WriteLine(product);
}
// Output: Bread, Cheese
Visualization:
| Warehouse | Sold | Intersect (Common) |
|---|---|---|
| Bread | Bread | Bread |
| Cheese | Cheese | Cheese |
Where is this useful?
- You want to know which products from your warehouse were sold today.
- Interviewers love to ask: "How do you find the intersection of two lists?" — LINQ does it in one line!
- In business apps, intersection is often needed for filtering by combined criteria.
Features and common mistakes
If an element appears multiple times in a collection, the result will only have one instance of that element.
For complex objects (like the Product class), again, you need proper Equals/GetHashCode implementations.
Example with objects
var commonObjects = stock.Intersect(sold);
foreach (var p in commonObjects)
Console.WriteLine($"{p.Name} ({p.Category})");
// Output: Bread (Bakery)
4. The Except Operation — Difference of Collections
Except returns elements that are only in the first collection, but not in the second.
Example
var productsOnlyInStock = warehouseProducts.Except(recentlySold);
foreach (var product in productsOnlyInStock)
{
Console.WriteLine(product);
}
// Output: Milk, Eggs
So, these are products that are in the warehouse, but haven't been sold.
Visualization:
| Warehouse | Sold | Except (only warehouse) |
|---|---|---|
| Milk | Milk | |
| Eggs | Eggs | |
| Bread | Bread | (skipped) |
| Cheese | Cheese | (skipped) |
Analogy
It's like if you removed from your stack all the documents you've already sent — you'll only have what's still with you and nowhere else.
Example with objects
var unsold = stock.Except(sold);
foreach (var p in unsold)
Console.WriteLine($"{p.Name} ({p.Category})");
// Output: Milk (Dairy)
! Not-so-obvious stuff
The Except method is order-sensitive: A.Except(B) is not the same as B.Except(A)! The first collection is "where you subtract from", the second is "what you remove".
5. Combining and Composing Multiple LINQ Set Operations
Sometimes one operation isn't enough. Say you want to show products that are either only in the warehouse or only in the sold list — but not in both at the same time (this is called "symmetric difference").
Symmetric difference ("XOR" for sets):
var onlyInOne = warehouseProducts.Except(recentlySold)
.Union(recentlySold.Except(warehouseProducts));
foreach (var product in onlyInOne)
{
Console.WriteLine(product);
}
// Output: Milk, Eggs, Salami, Tea
For more complex logic, it's handy to combine LINQ methods:
// Find products that are neither in the warehouse nor among the sold, but only in the "expected delivery" list
List<string> expected = new() { "Coffee", "Tea", "Milk" };
var onlyExpected = expected.Except(warehouseProducts.Union(recentlySold));
foreach (var product in onlyExpected)
Console.WriteLine(product);
// Output: Coffee
6. Working with Custom Types and IEqualityComparer
Sometimes you don't want to compare objects by all their fields: for example, you only care about the product name, and the category doesn't matter. For this, LINQ methods support an extra parameter — IEqualityComparer<T>, which defines how to compare elements.
Example of a custom comparer:
class ProductNameComparer : IEqualityComparer<Product>
{
public bool Equals(Product? x, Product? y) => x?.Name == y?.Name;
public int GetHashCode(Product obj) => obj.Name.GetHashCode();
}
var comp = new ProductNameComparer();
var uniqueByName = stock.Union(sold, comp);
foreach (var p in uniqueByName)
Console.WriteLine(p.Name); // Milk, Bread, Salami
This is especially handy if you don't want to change Equals/GetHashCode in your whole model, but just want to compare objects by a specific rule this one time.
7. Visual Schemes and Tables
Let's sum up the use cases (for lists A and B):
| Operation | Result |
|---|---|
|
Everything that's in A or B (unique elements). |
|
Everything that's in both (A and B). |
|
Everything that's only in A, but not in B. |
Diagram (Venn Diagram, if we could draw):
[A] [B]
oooooooooo
oooooooo oooo
ooooo oo ooo
ooo ooo oo
oo o ooo
ooo ooo ooo
- Union: everything inside both circles.
- Intersect: only the intersection (center).
- Except: only what's in circle A, not touching the overlap with B.
8. Common Mistakes When Using Union, Intersect, and Except
Mistake #1: Using with objects that don't have Equals and GetHashCode.
If your class doesn't override these methods, the Union, Intersect, and Except methods will work wrong: objects with the same content will be treated as different. You'll get unexpected (and often useless) results.
Mistake #2: Trying to compare objects by only some fields without IEqualityComparer.
For example, if you want to compare products only by name, not the whole structure, Intersect won't get it by default. Without an explicit IEqualityComparer, the result won't match what you expect.
Mistake #3: Wrong expectations about element order.
A lot of people think the final collection keeps the order of merging or intersection. But the behavior depends on the method: Union keeps the order of the first collection, but Intersect and Except might return elements in a random order. It's better not to rely on order at all.
Mistake #4: Ignoring performance when working with big collections.
If your data is huge, these methods can be slow. Think about pre-aggregating, filtering, or using hash structures (like HashSet) to speed things up.
GO TO FULL VERSION