Master the full LINQ method chain, including GroupBy, SelectMany, Join, Distinct, Take and Skip, Zip, Aggregate and set operations, and write your own extension operators.

Bishal Biswas
Self Learner
Extension methods
Every LINQ operator is an extension method on IEnumerable<T>. An extension method is a static method that you call as if it belonged to the type. That is why numbers.Where(...) works even though List<T> has no Where method of its own.
Here is a tiny one of your own, so the mechanism is clear:
static class StringExtensions
{
public static bool IsBlank(this string? s) => string.IsNullOrWhiteSpace(s);
}
Console.WriteLine(" ".IsBlank()); // True
The this on the first parameter is what makes it an extension. LINQ is a large collection of methods like this in System.Linq.Enumerable.
Partitioning: Take, Skip and friends
var numbers = Enumerable.Range(1, 10); // 1 to 10
numbers.Take(3); // 1 2 3
numbers.Skip(7); // 8 9 10
numbers.Skip(2).Take(3); // 3 4 5, this is paging
numbers.TakeWhile(n => n < 4); // 1 2 3, stops at the first failure
numbers.SkipWhile(n => n < 8); // 8 9 10
numbers.TakeLast(2); // 9 10
numbers.Chunk(4); // [1,2,3,4] [5,6,7,8] [9,10]
Paging a list is Skip((page - 1) * size).Take(size).
Distinct and set operations
var a = new[] { 1, 2, 2, 3 };
var b = new[] { 3, 4 };
a.Distinct(); // 1 2 3
a.Union(b); // 1 2 3 4
a.Intersect(b); // 3
a.Except(b); // 1 2
DistinctBy removes duplicates by a key, which is handy for objects:
people.DistinctBy(p => p.Email);
GroupBy
Groups items by a key. Each group has a Key and is a sequence of its members:
record Sale(string Region, string Product, decimal Amount);
var sales = new List<Sale>
{
new("North", "Laptop", 1200),
new("South", "Phone", 800),
new("North", "Phone", 700),
new("South", "Laptop", 1100)
};
var byRegion = sales.GroupBy(s => s.Region);
foreach (var group in byRegion)
{
Console.WriteLine($"{group.Key}: {group.Sum(s => s.Amount)}");
}
// North: 1900
// South: 1900
Project each group straight into a summary:
var summary = sales
.GroupBy(s => s.Region)
.Select(g => new
{
Region = g.Key,
Count = g.Count(),
Total = g.Sum(s => s.Amount),
Best = g.MaxBy(s => s.Amount)!.Product
})
.OrderByDescending(x => x.Total);
MaxBy and MinBy return the whole item with the largest or smallest key, which saves an OrderBy().First().
SelectMany: flattening
When each item contains a collection and you want one flat sequence:
record Team(string Name, List<string> Members);
var teams = new List<Team>
{
new("Backend", new() { "Asha", "Ravi" }),
new("Frontend", new() { "Meera" })
};
var everyone = teams.SelectMany(t => t.Members); // Asha Ravi Meera
var tagged = teams.SelectMany(t => t.Members, (t, m) => $"{m} ({t.Name})");
// Asha (Backend), Ravi (Backend), Meera (Frontend)
Join
record Customer(int Id, string Name);
record Order(int CustomerId, decimal Total);
var customers = new List<Customer> { new(1, "Asha"), new(2, "Ravi") };
var orders = new List<Order> { new(1, 50), new(1, 25), new(2, 80) };
var lines = customers.Join(
orders,
c => c.Id, // key from the outer sequence
o => o.CustomerId, // key from the inner sequence
(c, o) => $"{c.Name} spent {o.Total}");
GroupJoin gives each customer a collection of their orders instead of one row per order.
Aggregate: fold everything into one value
Sum, Min and Max are special cases. Aggregate lets you write any accumulation:
var words = new[] { "C#", "is", "fun" };
string sentence = words.Aggregate((acc, w) => acc + " " + w); // "C# is fun"
int product = new[] { 1, 2, 3, 4 }.Aggregate(1, (acc, n) => acc * n); // 24
The first form uses the first element as the seed. The second takes an explicit seed.
Zip: pair two sequences
var names = new[] { "Asha", "Ravi" };
var scores = new[] { 90, 75 };
var paired = names.Zip(scores, (n, s) => $"{n}: {s}"); // Asha: 90, Ravi: 75
Ordering by several keys
people
.OrderBy(p => p.LastName)
.ThenByDescending(p => p.Age)
.ThenBy(p => p.FirstName);
Using OrderBy twice would throw away the first ordering. Always use ThenBy for secondary keys. Reverse flips the whole sequence.
Sequence generators
Enumerable.Range(1, 5); // 1 2 3 4 5
Enumerable.Repeat("x", 3); // x x x
Enumerable.Empty<int>(); // an empty sequence of the right type
Conversion and lookup
var dict = people.ToDictionary(p => p.Id); // key to object
var byName = people.ToDictionary(p => p.Id, p => p.Name); // key to value
var lookup = orders.ToLookup(o => o.CustomerId); // key to many, never throws on duplicates
foreach (var o in lookup[1]) Console.WriteLine(o.Total);
ToLookup is what you want when one key maps to several items. ToDictionary throws on duplicate keys.
Index in Select
The overload of Select with two parameters passes the index:
var numbered = names.Select((name, i) => $"{i + 1}. {name}");
Writing your own operator
Because LINQ is just extension methods over IEnumerable<T>, you can add operators. yield return builds a lazy sequence:
static class MyLinq
{
public static IEnumerable<T> EveryOther<T>(this IEnumerable<T> source)
{
bool take = true;
foreach (T item in source)
{
if (take) yield return item;
take = !take;
}
}
}
Enumerable.Range(1, 6).EveryOther(); // 1 3 5
Your operator chains with all the built in ones and is just as lazy.
Quick recap
- LINQ operators are extension methods, and you can write your own with
thisandyield return. Take,Skip,TakeWhileandChunkslice sequences, andSkipplusTakeis paging.Distinct,Union,IntersectandExceptare set operations.GroupBygroups by key,SelectManyflattens,Joinmatches two sequences.Aggregatefolds a sequence into one value,Zippairs two sequences.- Use
ThenByfor secondary sort keys andToLookupfor one to many maps.
// LINQ method syntax
record Sale(string Region, string Product, decimal Amount);
var sales = new List<Sale>
{
new("North", "Laptop", 1200), new("South", "Phone", 800),
new("North", "Phone", 700), new("South", "Laptop", 1100),
new("North", "Tablet", 400)
};
// GroupBy with a projected summary, MaxBy picks the whole item
var summary = sales
.GroupBy(s => s.Region)
.Select(g => new { Region = g.Key, Total = g.Sum(s => s.Amount), Best = g.MaxBy(s => s.Amount)!.Product })
.OrderByDescending(x => x.Total);
foreach (var s in summary)
Console.WriteLine($"{s.Region}: {s.Total} (best: {s.Best})");
// Partitioning and paging
var numbers = Enumerable.Range(1, 10);
Console.WriteLine(string.Join(" ", numbers.Skip(2).Take(3))); // 3 4 5
Console.WriteLine(string.Join(" ", numbers.TakeWhile(n => n < 4))); // 1 2 3
Console.WriteLine(numbers.Chunk(4).Count()); // 3
// Set operations
int[] a = { 1, 2, 2, 3 }, b = { 3, 4 };
Console.WriteLine(string.Join(" ", a.Distinct())); // 1 2 3
Console.WriteLine(string.Join(" ", a.Union(b))); // 1 2 3 4
Console.WriteLine(string.Join(" ", a.Except(b))); // 1 2
// SelectMany flattens, Zip pairs, Aggregate folds
record Team(string Name, List<string> Members);
var teams = new List<Team> { new("Backend", new() { "Asha", "Ravi" }), new("Frontend", new() { "Meera" }) };
Console.WriteLine(string.Join(", ", teams.SelectMany(t => t.Members))); // Asha, Ravi, Meera
var scores = new[] { 90, 75, 88 };
Console.WriteLine(string.Join(", ", teams.SelectMany(t => t.Members).Zip(scores, (m, s) => $"{m}:{s}")));
Console.WriteLine(new[] { 1, 2, 3, 4 }.Aggregate(1, (acc, n) => acc * n)); // 24
// ToLookup for one key to many items
var lookup = sales.ToLookup(s => s.Product);
Console.WriteLine(lookup["Phone"].Count()); // 2
// A custom lazy operator using yield return
static class MyLinq
{
public static IEnumerable<T> EveryOther<T>(this IEnumerable<T> source)
{
bool take = true;
foreach (T item in source) { if (take) yield return item; take = !take; }
}
}
Console.WriteLine(string.Join(" ", Enumerable.Range(1, 6).EveryOther())); // 1 3 5
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready