Less Known LINQ Methods
Introduction
LINQ extension methods are pretty cool and they make our lives much easier. They are implemented as .NET extension methods over the Enumerable class in the System.Linq namespace and as usual they all apply to IEnumerable<T> sequences.
This time I'll be talking about some LINQ methods that are not so commonly used (as per my understanding). Not sure about you guys, but sometimes I don't even remember that some of these exist.
Aggregate/AggregateBy
Aggregate applies an accumulator function over a sequence, reducing it to a single value. AggregateBy (.NET 9+) does the same but groups by a key first, producing one accumulated result per key.
// Aggregate
var product = new[] { 1, 2, 3, 4 }.Aggregate((acc, x) => acc * x); // 24
// AggregateBy
var words = new[] { "apple", "ant", "bear", "bee" };
var lengthSums = words.AggregateBy(
keySelector: w => w[0],
seed: 0,
func: (acc, w) => acc + w.Length);
// 'a' -> 8, 'b' -> 7
Append/Prepend
Add a single element to the end (Append) or beginning (Prepend) of a sequence without mutating the original.
var numbers = new[] { 2, 3, 4 };
var withEnds = numbers.Prepend(1).Append(5); // 1, 2, 3, 4, 5
Chunk
Chunk splits a sequence into fixed-size arrays (chunks). The last chunk may be smaller.
var chunks = Enumerable.Range(1, 7).Chunk(3); // [1,2,3], [4,5,6], [7]
CountBy
CountBy counts elements grouped by a key, without allocating full groups (.NET 9+).
var words = new[] { "cat", "car", "dog", "duck", "diver" };
var counts = words.CountBy(w => w[0]); //count grouped by the first letter
// 'c' -> 2, 'd' -> 3
Except/ExceptBy
Except returns elements from the first sequence not present in the second. ExceptBy compares using a key selector instead of the whole element.
var a = new[] { 1, 2, 3, 4 };
var b = new[] { 2, 4 };
var diff = a.Except(b); // 1, 3
var people = new[] { ("Red", 1), ("Green", 2), ("Blue", 3) };
var excludeIds = new[] { 2 };
var filtered = people.ExceptBy(excludeIds, p => p.Item2); // Red, Blue
FullJoin
FullJoin returns all rows from both sequences, matching where keys align and filling with default/null where there's no match on either side (will come in .NET 11+).
var employees = new[] { (Id: 1, Name: "Ana"), (Id: 2, Name: "Ricardo") };
var managers = new[] { (Id: 2, Manager: "Joao"), (Id: 3, Manager: "Jemma") };
var result = employees.FullJoin(
managers,
e => e.Id,
m => m.Id,
(e, m) => new { e?.Name, m?.Manager });
// (Ana, null), (Ricardo, Joao), (null, Jemma)Index
Index pairs each element with its zero-based index, avoiding manual counters (.NET 9+).
foreach (var (index, value) in new[] { "a", "b", "c" }.Index())
{
Console.WriteLine($"{index}: {value}");
}
// 0: a, 1: b, 2: c
Intersect/IntersectBy
Intersect returns elements present in both sequences. IntersectBy compares by a key instead of the full element.
var a = new[] { 1, 2, 3 };
var b = new[] { 2, 3, 4 };
var common = a.Intersect(b); // 2, 3
var products = new[] { (Id: 1, Name: "A"), (Id: 2, Name: "B") };
var activeIds = new[] { 2 };
var active = products.IntersectBy(activeIds, p => p.Id); // (2, "B")
LeftJoin/RightJoin
LeftJoin keeps every element from the left sequence, pairing with matches from the right (or default if none). RightJoin is the mirror image, keeping every element from the right sequence.
var employees = new[] { (Id: 1, Name: "Ana"), (Id: 2, Name: "Ricardo") };
var managers = new[] { (Id: 2, Manager: "Jemma") };
var left = employees.LeftJoin(
managers, e => e.Id, m => m.Id,
(e, m) => new { e.Name, Manager = m?.Manager });
// (Ana, null), (Ricardo, Jemma)
MaxBy/MinBy
Returns the element with the maximum (MaxBy)/minimum(MinBy) value of a selected key (not the item itself).
var people = new[] { (Name: "Ana", Age: 30), (Name: "Ricardo", Age: 25) };
var youngest = people.MinBy(p => p.Age); // (Ricardo, 25)
Order
Order sorts elements using their natural comparer - shorthand for OrderBy(x => x). Of course, the element itself must be comparable! You can also provide your own comparer (IComparer<T>).
var sorted = new[] { 3, 1, 2 }.Order(); // 1, 2, 3
SkipWhile
SkipWhile skips elements from the start as long as a condition is true, then returns the rest (even if the condition becomes true again later).
var numbers = new[] { 1, 2, 3, 4, 1 };
var result = numbers.SkipWhile(n => n < 3); // 3, 4, 1
TakeLast
TakeLast returns the last N elements of a sequence.
var lastTwo = new[] { 1, 2, 3, 4, 5 }.TakeLast(2); // 4, 5
TakeWhile
TakeWhile takes elements from the start as long as a condition is true, stopping at the first failure.
var numbers = new[] { 1, 2, 3, 4, 1 };
var result = numbers.TakeWhile(n => n < 3); // 1, 2
ToLookup
ToLookup builds an immutable, multi-value dictionary-like ILookup<K, V> structure (a key can map to multiple values), evaluated immediately.
var words = new[] { "apple", "ant", "bear", "bee" };
var lookup = words.ToLookup(w => w[0]);
foreach (var w in lookup['a'])
{
Console.WriteLine(w); // apple, ant
}
Union/UnionBy
Union returns distinct elements from both sequences combined. UnionBy deduplicates using a key selector.
var a = new[] { 1, 2, 3 };
var b = new[] { 3, 4, 5 };
var combined = a.Union(b); // 1, 2, 3, 4, 5
var people = new[] { (Id: 1, Name: "Ana") };
var more = new[] { (Id: 1, Name: "Ana"), (Id: 2, Name: "Ricardo") };
var uniquePeople = people.UnionBy(more, p => p.Id);
// (1, Ana), (2, Ricardo) — first occurrence wins
Zip
Zip combines two (or three) sequences element-by-element into tuples or a projection, stopping at the shortest sequence.
var names = new[] { "Ana", "Ricardo" };
var ages = new[] { 30, 25 };
var zipped = names.Zip(ages, (n, a) => $"{n} is {a}");
// "Ana is 30", "Ricardo is 25"
Conclusion
As always, I hope you find this useful: a few less-known extension methods with a simple example for each. Give me a shout if you feel anything is missing!
Comments
Post a Comment