← Open in the full interactive course (progress tracking, search & more)

Where and Select aren't magic language features — they're just extension methods somebody wrote. You can write your own.

Every LINQ operator used across this entire module is, mechanically, nothing more than a static extension method on IEnumerable<T> — the previous lesson said so directly. Microsoft doesn't have some private compiler trick unavailable to you. If Where and Select are just methods, you can write a method exactly like them, that chains into a LINQ query exactly the way they do — and once your codebase has a filtering pattern it repeats constantly, that's precisely when you should.

This lesson covers writing your own LINQ-style extension method on IEnumerable<T> — using a practical WhereNotNull helper as the running example — how it connects back to the extension methods from Intermediate Part III, and how to preserve deferred execution correctly using yield return.

What Is It?

The Simple Explanation

A custom LINQ extension is your own static method, written the same way Where or Select is, that extends IEnumerable<T> and can be dot-chained into a LINQ query alongside every built-in operator — because to the compiler, there's no difference between yours and Microsoft's.

The Technical Definition

An extension method requires exactly three things: a static method, inside a static class, whose first parameter is marked with this:

public static class EnumerableExtensions { public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : class { foreach (var item in source) { if (item is not null) yield return item; } } }

The this IEnumerable<T?> source parameter is what lets you call it as products.WhereNotNull() instead of EnumerableExtensions.WhereNotNull(products) — identical to how the compiler treats every built-in LINQ method, as the previous lesson's "Under the Hood" section explained.

Why Does It Exist?

Some filtering and shaping patterns show up over and over across a codebase, but LINQ has no built-in operator for them. Without a custom extension, that repeated logic gets copy-pasted, or buried inline, every single time:

// The same "skip the nulls" logic, rewritten inline, again and again var validNames = rawNames.Where(n => n is not null).Select(n => n!); // needs a null-forgiving operator too var validEmails = rawEmails.Where(e => e is not null).Select(e => e!); var validAddresses = rawAddresses.Where(a => a is not null).Select(a => a!);
// Written once, reused everywhere, and the compiler now KNOWS the result has no nulls var validNames = rawNames.WhereNotNull(); var validEmails = rawEmails.WhereNotNull(); var validAddresses = rawAddresses.WhereNotNull();

Beyond avoiding repetition, WhereNotNull() genuinely improves on the manual version: it returns IEnumerable<T> (not T?) after filtering, so the compiler's nullable reference type analysis correctly understands that every remaining item is guaranteed non-null — no null-forgiving ! operator required downstream. A well-designed custom extension doesn't just save keystrokes; it can express an intent LINQ's built-in operators don't capture on their own.

Big Picture

A CUSTOM EXTENSION SLOTS INTO A CHAIN LIKE ANY BUILT-IN ONE
rawEmails .WhereNotNull() — YOUR method .Where(e => e.EndsWith(".com")) — Microsoft's method .OrderBy(e => e) — Microsoft's method
To the compiler, and to anyone reading the chain, there's no visible seam between them.

How It Works

WRITING A CUSTOM LINQ EXTENSION, STEP BY STEP
1. PUT IT IN A static CLASS
2. MAKE THE METHOD static, WITH this ON THE FIRST PARAMETER
3. USE yield return TO PRESERVE DEFERRED EXECUTION AND STREAMING
4. CHAIN IT LIKE ANY OTHER LINQ OPERATOR

Simple Example

public static class EnumerableExtensions { // A custom, LINQ-style extension — filters out nulls, and tells the // compiler the result type no longer contains any public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : class { ArgumentNullException.ThrowIfNull(source); return WhereNotNullIterator(source); } // The actual iterator lives in a separate, private method — explained in Under the Hood private static IEnumerable<T> WhereNotNullIterator<T>(IEnumerable<T?> source) where T : class { foreach (var item in source) { if (item is not null) yield return item; } } } string?[] rawNames = ["Ana", null, "Ben", null, "Cara"]; IEnumerable<string> validNames = rawNames.WhereNotNull(); foreach (var name in validNames) Console.WriteLine(name); // Ana // Ben // Cara

Code → Meaning → Result: Notice the two-method split — a public method that validates arguments immediately, and a private method holding the actual yield return loop. That split isn't cosmetic; it fixes a real, subtle bug that's the centerpiece of this lesson's Under the Hood section below.

Chained Straight Into a Real Query

public record Product(int Id, string Name, string? PromoCode); List<Product> products = [ new(1, "Wireless Mouse", "SAVE10"), new(2, "Standing Desk", null), new(3, "Desk Lamp", "SAVE5"), ]; // WhereNotNull sits comfortably alongside Where, Select, and OrderBy — // the compiler treats it no differently var activePromos = products .Select(p => p.PromoCode) .WhereNotNull() .OrderBy(code => code); foreach (var code in activePromos) Console.WriteLine(code); // SAVE10 // SAVE5

Real-World Example

A products catalog frequently needs to check whether an item is genuinely purchasable — in stock, and not discontinued. Rather than repeat that two-part condition in every query across the codebase, wrap it in a domain-specific extension:

public record Product(int Id, string Name, decimal Price, int Stock, bool IsDiscontinued); public static class ProductQueryExtensions { // A domain-specific filter — reads like business language, not raw conditions public static IEnumerable<Product> WherePurchasable(this IEnumerable<Product> products) { ArgumentNullException.ThrowIfNull(products); return WherePurchasableIterator(products); } private static IEnumerable<Product> WherePurchasableIterator(IEnumerable<Product> products) { foreach (var p in products) { if (p.Stock > 0 && !p.IsDiscontinued) yield return p; } } } // Used across the codebase — search, recommendations, checkout validation, etc. var searchResults = catalog.WherePurchasable().Where(p => p.Name.Contains(searchTerm)); var recommendations = catalog.WherePurchasable().OrderByDescending(p => p.Price).Take(5);

The business rule "purchasable means in stock and not discontinued" is now defined in exactly one place. If that rule ever changes — say, a new "temporarily unavailable" flag is added — there's a single method to update, instead of hunting down every scattered p.Stock > 0 && !p.IsDiscontinued condition across the codebase. This is precisely the same DRY motivation behind extension methods generally, from Intermediate Part III, applied directly to LINQ.

Analogy

A Custom LEGO Brick That Still Snaps In

Every LEGO brick, official or not, follows the exact same stud spacing — that's what lets any two bricks connect. If you 3D-print your own brick shaped for a purpose the official sets don't cover, as long as you match the standard stud pattern, it snaps into the same builds as any official piece, indistinguishable in how it connects. Writing a custom LINQ extension is exactly this: match the required shape (a static method, this IEnumerable<T>, returning another IEnumerable<T>), and it snaps into any LINQ chain exactly like a built-in piece.

Under the Hood

THE HIDDEN GOTCHA: DEFERRED ARGUMENT VALIDATION
1. A yield return METHOD BODY BECOMES AN ENTIRE STATE MACHINE — INCLUDING YOUR VALIDATION CODE
2. WHY THAT'S A REAL BUG, NOT JUST A THEORETICAL CONCERN
3. THE FIX — SPLIT VALIDATION FROM ITERATION, THE PATTERN USED IN EVERY EXAMPLE ABOVE

Common Confusion

1. Extension methods vs. instance methods on your own types

If you own the Product class yourself, you could add a regular instance method to it. Extension methods matter specifically because IEnumerable<T> is an interface you don't own and can't modify — you cannot add a method directly to it. Extension methods are the only way to make any sequence, regardless of its underlying concrete type, gain a new chainable operation, which is exactly why this is how all of LINQ itself is built.

2. A custom extension that builds a List<T> internally isn't really "LINQ-style" anymore

Nothing stops you from writing public static IEnumerable<T> MyFilter<T>(this IEnumerable<T> source, ...) { var result = new List<T>(); foreach(...) ... return result; } — it compiles, and it even works. But it silently breaks the "lazy, streaming" contract every built-in LINQ operator honors: it forces the entire source to be consumed the moment it's called, rather than deferring, and it can't short-circuit for something like an infinite sequence combined with Take. Using yield return, as this lesson's examples do, is what keeps a custom extension behaving indistinguishably from a real LINQ operator.

Common Mistakes

Mistake 1 — Putting argument validation directly inside a yield return method

public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : class { ArgumentNullException.ThrowIfNull(source); foreach (...) if (...) yield return item; } — the null check is silently deferred along with everything else. Split the method in two, exactly as shown throughout this lesson: an outer method that validates immediately and calls a private iterator method for the yield return logic.

Mistake 2 — Naming a custom extension the same as an existing LINQ method, with different behavior

Writing your own Where extension that behaves subtly differently from Enumerable.Where — extremely confusing for anyone reading the code, since they'll assume the standard, well-known behavior. Give custom extensions distinct, descriptive names — WhereNotNull, WherePurchasable — that make it obvious this isn't one of the built-in operators.

When Should I Use It?

Write a custom LINQ extension when

Skip it when

Mental Model

Custom LINQ extension = "a method shaped exactly like Where, that snaps into a chain exactly like Where"

Remember:
· static class, static method, this IEnumerable<T> first parameter — that's the whole recipe.
· Use yield return to keep it lazy and streaming, matching every built-in operator's behavior.
· Split eager validation from the lazy iterator — validate in an outer method, iterate in a private one.
· Reach for one when a pattern repeats or a domain concept deserves a readable name of its own.

Key Takeaway


Check Your Understanding

You've learned how to write your own LINQ-style extension method. Let's check your understanding.

1. What are the three requirements for a method to be usable as source.MyMethod(), exactly like a built-in LINQ operator?

Show answer

Correct: B

Why B is correct: As shown in What Is It? and How It Works, these three requirements — static method, static class, this on the first parameter — are the entire, complete recipe for an extension method in C#, with no additional registration step of any kind.

Why A is incorrect: The entire point of extension methods, as covered in Common Confusion, is extending a type you don't own — IEnumerable<T> — not adding a method inside a class you control.

Why C is incorrect: The extension method itself doesn't implement any interface — it operates on a parameter typed as IEnumerable<T>.

Why D is incorrect: There is no registration file or step — the compiler discovers extension methods purely from their signature and an appropriate using directive for their containing namespace.

Reinforcement: Match this exact shape, and any method becomes chainable into a LINQ query just like a built-in operator.

2. Why does this lesson split WhereNotNull into two methods — a public one and a private WhereNotNullIterator — instead of writing it as a single method?

Show answer

Correct: C

Why C is correct: As explained in Under the Hood, an entire method body containing yield return becomes a state machine — any validation code inside it runs only once MoveNext() is first called, not when the method itself is invoked. Splitting into an eager outer method and a lazy inner iterator fixes exactly this.

Why A is incorrect: There's no such language requirement — a single-method version compiles and runs; it just has the deferred-validation bug this lesson explains.

Why B is incorrect: The split has no meaningful runtime performance effect — it's purely about when the argument-null check actually executes.

Why D is incorrect: This is a real, functional bug fix, not a stylistic choice — it changes exactly when an exception is thrown for invalid input, and where in the code that failure is reported.

Reinforcement: Whenever you write a yield return method that also needs eager validation, split it into a public validating method and a private iterator — this is the same pattern .NET's own Enumerable.Where uses internally.

3. A custom extension is written that eagerly builds and returns a List<T> internally, instead of using yield return. What's the practical downside?

Show answer

Correct: B

Why B is correct: As covered in Common Confusion, building an internal List<T> forces immediate, full enumeration of the source the instant the method is called — breaking the "recipe, not a result" deferred behavior every other operator in this module has consistently had.

Why A is incorrect: It compiles and runs perfectly fine — yield return is a choice for preserving good behavior, not a compiler requirement.

Why C is incorrect: It doesn't throw — it simply behaves differently (eagerly instead of lazily), which can cause subtle bugs or performance surprises rather than a crash.

Why D is incorrect: The behavioral difference — eager vs. deferred — is exactly the point being tested; they are not identical.

Reinforcement: Using yield return is what keeps a custom extension truly indistinguishable from a built-in LINQ operator, not just similarly named.

4. A team keeps repeating p.Stock > 0 && !p.IsDiscontinued across dozens of different LINQ queries throughout the codebase. What does this lesson recommend?

Show answer

Correct: B

Why B is correct: As shown in the Real-World Example, wrapping a repeated business condition in a custom extension like WherePurchasable() centralizes the rule in one place and gives it a readable, domain-specific name — exactly the DRY motivation this lesson connects back to extension methods generally.

Why A is incorrect: This is precisely the repetition problem the Why Does It Exist? section identifies as the reason to write a custom extension in the first place.

Why C is incorrect: You cannot redefine or override the built-in Where method itself — and doing so would silently change the behavior of ordinary Where calls everywhere else in the codebase, which is far more dangerous than the repetition it would "solve."

Why D is incorrect: A record's constructor validates whether an object can be created at all — it has nothing to do with querying whether an already-valid, existing product happens to be purchasable right now, which can change over time (stock running out, for instance).

Reinforcement: A custom LINQ extension is the idiomatic fix for a repeated, meaningful condition — it turns a scattered rule into a single, named, reusable piece of vocabulary.

You can now extend LINQ with your own reusable, chainable operators. Next up, and closing out this module: what actually costs performance in LINQ code, and how to write it well.


dotnetmadeeasy.com — Learn C# and .NET, the right way.