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.
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.
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.
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.
rawEmails
.WhereNotNull() — YOUR method
.Where(e => e.EndsWith(".com")) — Microsoft's method
.OrderBy(e => e) — Microsoft's methodstatic class — by convention, often named {Type}Extensions, like EnumerableExtensions.this IEnumerable<T> source is what makes it callable as source.YourMethod() rather than YourClass.YourMethod(source).yield return — instead of eagerly building a List<T> and returning that — keeps your extension behaving exactly like the built-in ones: lazy, and able to produce results one at a time as it's enumerated.source.YourMethod().Where(...) works exactly the same as source.Where(...).Select(...) — there's no special registration step or interface to implement beyond the method signature itself.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
// CaraCode → 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.
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
// SAVE5A 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.
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.
yield return method body into a state machine class. If you put ArgumentNullException.ThrowIfNull(source) directly inside a method that also contains yield return, that check gets swept into the state machine too — meaning it doesn't run when the method is called, only when the result is first enumerated.null to a naive, single-method version of WhereNotNull would get no exception at all at the call site — the bug would only surface much later, possibly far away in the code, the moment something finally does a foreach over the result. That's a genuinely confusing debugging experience: the stack trace points to the enumeration site, not the actual mistake.yield return method for the actual lazy work. This is exactly the pattern used in every example in this lesson — and it's the same pattern the real Enumerable.Where implementation in .NET itself uses internally, for precisely this reason.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.
List<T> internally isn't really "LINQ-style" anymoreNothing 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.
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.
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.
Where(...) lambda is simpler and doesn't need a name.Where, that snaps into a chain exactly like Where"static class, static method, this IEnumerable<T> first parameter — that's the whole recipe.yield return to keep it lazy and streaming, matching every built-in operator's behavior.static method with a this IEnumerable<T> first parameter — the exact same shape every built-in LINQ operator uses.yield return to preserve deferred execution and streaming, exactly like Where and Select.WhereNotNull, WherePurchasable) captures a repeated pattern or domain concept in one reusable, chainable place.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?
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?
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?
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?
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.