Sitemap

Unlocking the Secret Features of LINQ

5 min readApr 17, 2026

--

Press enter or click to view image in full size

Previously, I wrote an article about a handy ‘weird’ method that prevents empty collections.

var empty = Enumerable.Empty<int>();
var result = empty.DefaultIfEmpty(42); // [42]

This is especially useful in aggregations to avoid exceptions or empty results.

var maxNumber = empty
.Where(n => n > 25)
.DefaultIfEmpty()
.Max(); // no longer throw InvalidOperationException("Sequence contains no elements")

You seem to like it 🙃.

Therefore, let’s explore a few more weird LINQ method overloads.

Extension methods

Let’s start with something simple, and something you already know😅.

LINQ is not magic, but just a bunch of extension methods for IEnumerable<T>:

static class Enumerable
{
. . .
public static T First<TSource>(this IEnumerable<T> source);
public static float? Max(this IEnumerable<float?> source);
public static double? Sum(this IEnumerable<double?> source);
. . .
}

It means two things:

  • to make LINQ work with your own class, you need to inherit IEnumerable
  • while it is common to chain them, you can just call them as static methods:
var maxValue1 = new int[] { 1, 2, 3 }.Max();  // chaining
var maxValue2 = Enumerable.Max([1, 2, 3]); // use a static method

Static LINQ Methods

LINQ provides several handy static methods in the Enumerable class to generate sequences on the fly, without needing to manually create or fill collections.

IEnumerable<int> empty  = Enumerable.Empty<int>();                  // []
IEnumerable<int> range = Enumerable.Range(start: 2, count: 3); // [2, 3, 4]
IEnumerable<int> repeat = Enumerable.Repeat(element: 27, count: 2); // [27, 27]

And don’t forget — LINQ also offers a simple way to compare collections element-by-element 😉:

var array1 = new[] { 1, 2, 3 };
var array2 = new[] { 1, 2, 4 };

var areEqual1 = Enumerable.SequenceEqual(array1, array2);
var areEqual2 = array1.SequenceEqual(array2);

.Where((value, index) => predicate)

Many developers know popular LINQ methods — such as Select, Where, or SkipWhile. However, only a few are aware that they have overloads that accept two arguments: the current item and its index.

var result = numbers.Where((currentItem, index) => index % 2 == 0);

Here, x is the current element, and i is its index in the sequence.

.FirstOrDefault()

In all those, .FirstOrDefault(), .LastOrDefault(), .SingleOrDefault(), or .SomethingSomethingOrDefault() you can specify a default value.

var result = numbers.FirstOrDefault(42); // returns 42 instead of default(T)

This makes it much clearer and safer when you want a custom fallback value instead of relying on the type’s default.

.SelectMany()

As you know .SelectMany() allows you to flatten multidimensional collections into a single sequence.

var people = new[]
{
new { Name = "Alice", Pets = new[] { "Dog", "Cat" } },
new { Name = "Bob", Pets = new[] { "Fish" } }
};

// ["Dog", "Cat", "Fish"]
var allPets = people.SelectMany(p => p.Pets);

But, did you know that it also has a second parameter that lets you project both the outer and inner elements into a custom shape? 🤔

// [ { Alice - Dog }, { Alice - Cat }, { Bob - Fish } ]
var petOwners = people
.SelectMany(
p => p.Pets, // collectionSelector
(person, pet) => new { Owner = person.Name, Pet = pet } // resultSelector
);

.Cast()

.Cast() is useful for explicit casting, especially when working with non-generic sequences.

List<object> list = new List<object> { 1, 2, 3 };

List<int> numbers = list.Cast<int>().ToList();

Now, let’s say you have a base class and a derived class:

class Animal(string Name) { }
class Dog(string Name, string Breed) : Animal(Name) { }

And then work with collections of the derived type:

IEnumerable<Dog> dogs = new List<Dog>
{
new Dog("Buddy", "Golden Retriever"),
new Dog("Rex", "German Shepherd")
};

If you want to create a collection of base entities, you can use .Cast(), but you can also convert directly to the list with .ToList<BaseType>():

var animals1 = dogs.Cast<Animal>().ToList();
var animals2 = dogs.ToList<Animal>();
var animals3 = dogs.AsEnumerable<Animal>();

.GroupBy()

.GroupBy() groups elements by a specified key.

var people = new[]
{
new { Name = "Alice", City = "London" },
new { Name = "Bob", City = "Paris" },
new { Name = "Carol", City = "London" },
};

// London → [{ Alice }, { Carol }]
// Paris → [{ Bob }]
var groups = people.GroupBy(p => p.City);

You can also specify what each group should contain using an element selector:

// London → ["Alice", "Carol"]
// Paris → ["Bob"]
var namesByCity = people.GroupBy(p => p.City, p => p.Name);

But did you know, you can go even further, and you can project each group into a custom result using a result selector:

// London → { City = London, Count = 2 }
// Paris → { City = Paris, Count = 1 }
var cityStats = people.GroupBy(
p => p.City,
p => p.Name,
(string city, IEnumerable<string> names) => new
{
City = city,
Count = names.Count(),
}
);

This form is especially useful when you want an aggregated result right away — like counts, averages, or summaries — without iterating over groups later.

.Concat(), .Union(), .Zip()

LINQ has methods that allow you to combine collections into one, let’s see the difference between those:

Get iamprovidence’s stories in your inbox

Join Medium for free to get updates from this writer.

.Concat() allows you to combine multiple sequences into one. It simply appends the second sequence after the first:

var numbers1 = new List<int> { 1, 2 };
var numbers2 = new List<int> { 3, 4, 5 };

var result = numbers1.Concat(numbers2).ToList(); // [1, 2, 3, 4, 5]

.Union() is similar to .Concat(), but it removes duplicates, returning only distinct elements:

var numbers1 = new List<int> { 1, 2, 3, 4, 5 };
var numbers2 = new List<int> { 3, 4, 5 };

var result = numbers1.Union(numbers2).ToList(); // [1, 2, 3, 4, 5]

.Zip() is the one everybody forgot😁. It combines two sequences element by element, producing a new sequence based on a selector function:

var numbers1 = new List<int> { 1, 2 };
var numbers2 = new List<int> { 3, 4, 5 };

var result = numbers1.Zip(numbers2).ToList(); // [(1, 3), (2, 4)]
var result = numbers1.Zip(numbers2, (n1, n2) => n1 * n2).ToList(); // [3, 8]

Note: .Zip() stops when the shorter sequence ends.

.Aggregate()

.Aggregate() is one of the most powerful and sometimes confusing LINQ methods. Anyone who has used it knows they’ve been through some wild times 😅.

It can be used to generalize all sorts of accumulation operations (like Sum, Max, etc).

var numbers = new[] { 1, 2, 3, 4 };

// 10
var sum = numbers.Aggregate((result, currentItem) => result + currentItem);

But it goes beyond that. Only a few know that one of its overloads allows you to set an initial value:

// 20
var sum = numbers.Aggregate(
10, // seed
(result, currentItem) => result + currentItem); // accumulation function

And, only developers from Microsoft know (but maybe even they forget) that there is an overload that lets you transform the final accumulator before returning it:

var words = new[] { "Hello", "World" };

var sentence = words.Aggregate(
"", // seed
(result, word) => result + word + " ", // accumulation function
result => result.Trim() // final transformation
);

Console.WriteLine(sentence); // "Hello World"

Summary

LINQ is full of surprises that few developers ever notice. So next time you write a .Where(), .Select(), or .Aggregate(), remember: there’s probably a secret overload or quirky behavior waiting to make your code a little more expressive (and maybe a little weirder 😄).

Let me know in the comment section is there's anything else I forgot about LINQ💬 Clap if you like the article👋 Support me with a coffee ☕ Follow to receive more ✅ See U! 😜

--

--

iamprovidence
iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence