100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Common LINQ Patterns

The recurring LINQ idioms — filtering, flattening, grouping, and joining — that show up in almost every C# codebase.

Practical LINQIntermediate9 min readJul 10, 2026
Analogies

Filtering and Existence-Check Patterns

The most common LINQ pattern by far is filter-then-fetch-one: Where narrows a sequence, and FirstOrDefault (or SingleOrDefault when exactly one match is expected) retrieves a specific element, returning null or a default value instead of throwing when nothing matches. A closely related pattern is the existence check: prefer Any(x => predicate) over Where(...).Count() > 0 or Count(...) > 0, because Any stops as soon as it finds the first match, while Count must enumerate the entire sequence even when you only care whether anything matched at all.

🏏

Cricket analogy: It's like a selector checking 'has any Under-19 player scored a double century this season?' — Any stops the moment it finds MD Kartik's double hundred, while counting every century first (like tallying every ton before checking if the count is above zero) wastes effort scanning the whole domestic season.

Projection and Flattening Patterns

Select projects each element to a new shape, but when each element itself contains a collection — orders containing line items, blog posts containing comments — SelectMany flattens those nested collections into a single sequence. orders.SelectMany(o => o.Items) returns every item across every order as one flat IEnumerable<OrderItem>, which is the pattern behind almost any 'give me all the X across all the Y' requirement, and it can be combined with a result selector to keep a reference back to the parent, as in orders.SelectMany(o => o.Items, (o, i) => new { o.Id, i.ProductName }).

🏏

Cricket analogy: It's like flattening every over bowled across every match in a series into one ball-by-ball list — SelectMany turns a series (a collection of matches, each with a collection of overs) into a single flat sequence of deliveries for analysis.

csharp
// SelectMany flattens each order's items into one sequence,
// pairing each item back with its parent order id.
var allItemsWithOrderId = orders.SelectMany(
    order => order.Items,
    (order, item) => new { order.Id, item.ProductName, item.Quantity }
);

// GroupBy + aggregate: total quantity sold per product across all orders.
var totalsByProduct = orders
    .SelectMany(o => o.Items)
    .GroupBy(i => i.ProductName)
    .Select(g => new { Product = g.Key, TotalQuantity = g.Sum(i => i.Quantity) })
    .OrderByDescending(x => x.TotalQuantity);

Grouping and Aggregation Patterns

GroupBy(x => x.Key) produces a sequence of IGrouping<TKey, TElement>, letting you iterate distinct keys each paired with its matching elements, and it is almost always followed by an aggregate operator like Sum, Average, Count, or Max applied per group. A related but distinct tool is ToLookup, which behaves like an immediately-executed, indexable version of GroupBy — a Lookup<TKey, TElement> can be queried by key like a dictionary (lookup["Electronics"]) without throwing if the key is missing, which makes it a good fit when you need repeated key-based access to grouped data rather than a single pass.

🏏

Cricket analogy: It's like grouping every innings in a tournament by team and then summing runs per team to build the points table — GroupBy produces the grouping, Sum produces the table, and a ToLookup-style index lets commentators instantly pull up 'Mumbai Indians' innings' without rescanning the tournament.

Set and Join Patterns

When combining two sequences by a related key, Join mirrors an inner join — customers.Join(orders, c => c.Id, o => o.CustomerId, (c, o) => new { c.Name, o.Total }) — returning one result per matching pair and dropping customers with no orders, while GroupJoin produces one result per left-side element paired with all its matches (including an empty collection when there are none), which is the LINQ equivalent of a left outer join when combined with DefaultIfEmpty. For comparing whole sequences, Distinct removes duplicates, Union combines two sequences and removes duplicates across both, Intersect keeps only elements present in both, and Except keeps elements present in the first sequence but not the second.

🏏

Cricket analogy: It's like joining a list of registered players with a list of match scorecards by player ID to get each player's runs — Join drops players who didn't play that match, while GroupJoin (a left outer join) keeps every registered player even those who sat out, showing an empty scorecard for them.

Distinct(), Union(), Intersect(), and Except() all use the default equality comparer for the element type unless you pass an IEqualityComparer<T> — for reference types without an overridden Equals, this compares by reference, which is a common source of 'Distinct isn't removing my duplicates' bugs.

  • Any(predicate) short-circuits on the first match and is far cheaper than Count(predicate) > 0 for existence checks.
  • SelectMany flattens a sequence-of-sequences into one flat sequence, the standard pattern for 'all X across all Y'.
  • GroupBy pairs each distinct key with its matching elements and is almost always followed by an aggregate like Sum or Count.
  • ToLookup is an eagerly-executed, indexable alternative to GroupBy for repeated key-based lookups.
  • Join behaves like an inner join (drops non-matches); GroupJoin plus DefaultIfEmpty behaves like a left outer join.
  • Distinct, Union, Intersect, and Except compare using the default equality comparer unless a custom IEqualityComparer<T> is supplied.

Practice what you learned

Was this page helpful?

Topics covered

#LINQStudyNotes#MicrosoftTechnologies#CommonLINQPatterns#Common#LINQ#Patterns#Filtering#StudyNotes#SkillVeris#ExamPrep