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

Popular posts from this blog

Modern Mapping with EF Core

OpenTelemetry with ASP.NET Core

.NET 10 Validation