Why Write Custom LINQ Operators
The built-in LINQ operators (Where, Select, GroupBy, and so on) are all implemented as static extension methods on IEnumerable<T> in the System.Linq namespace. You can write your own extension methods in exactly the same style — a static method in a static class, taking 'this IEnumerable<T> source' as its first parameter — to encapsulate reusable query logic that isn't covered by the standard library, such as batching a sequence into fixed-size chunks, computing a running total, or filtering out consecutive duplicates, while still getting fluent, chainable syntax like source.MyCustomOperator().Where(...).
Cricket analogy: Writing a custom operator is like a franchise inventing its own bespoke fielding drill beyond the standard warm-up routines every team uses, tailored to a specific weakness, yet still run using the same training-ground format as every other drill.
Deferred Execution with yield return
To match the deferred execution behavior of the built-in operators, a custom LINQ extension method should be implemented as an iterator using yield return rather than eagerly building a List<T> and returning it. This means the method body doesn't run at all until the caller starts enumerating the result; each MoveNext() call on the returned enumerator pulls exactly one more element through the pipeline, which keeps memory usage low and allows the operator to compose correctly with Take(), FirstOrDefault(), and infinite sequences.
Cricket analogy: yield return is like a bowler delivering one ball only when the batter is actually ready at the crease, rather than bowling an entire pre-planned over into an empty net beforehand; each delivery happens on demand, exactly when needed.
public static class MyLinqExtensions
{
// Batches a sequence into fixed-size chunks, using deferred execution
public static IEnumerable<List<T>> Batch<T>(this IEnumerable<T> source, int size)
{
if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
List<T>? batch = null;
foreach (var item in source)
{
batch ??= new List<T>(size);
batch.Add(item);
if (batch.Count == size)
{
yield return batch;
batch = null;
}
}
if (batch is { Count: > 0 })
yield return batch;
}
// Filters out consecutive duplicates
public static IEnumerable<T> DistinctUntilChanged<T>(this IEnumerable<T> source)
{
using var e = source.GetEnumerator();
if (!e.MoveNext()) yield break;
var previous = e.Current;
yield return previous;
while (e.MoveNext())
{
if (!EqualityComparer<T>.Default.Equals(previous, e.Current))
{
previous = e.Current;
yield return previous;
}
}
}
}
// Usage: chains fluently with built-in operators
var batches = orders.Where(o => o.IsActive).Batch(50);
foreach (var page in batches) { /* process 50 at a time */ }Argument Validation and Eager Guard Clauses
Because iterator methods with yield return don't run any code until the first MoveNext() call, naive argument validation (like a null check on the source parameter) placed directly in an iterator block won't throw until enumeration begins — often surprising callers who expect validation errors immediately when the method is called. The standard fix is to split the method into a public non-iterator wrapper that validates arguments eagerly and throws immediately, then delegates to a private iterator method that does the actual yield return work.
Cricket analogy: This is like a match referee checking that both teams have submitted valid playing XIs before the toss even happens, rather than only discovering a missing player once the first over is already underway.
The public/private split pattern — a thin public method that validates eagerly and a private iterator (often suffixed Core or Iterator) that does the deferred work — is exactly what the .NET team uses internally for operators like Enumerable.Where and Enumerable.Select, so following it makes your custom operators behave consistently with the rest of LINQ.
- Custom LINQ operators are static extension methods on IEnumerable<T>, matching the built-in operator style.
- Use yield return to implement deferred execution, matching the laziness of Where, Select, and similar operators.
- Deferred iterator methods don't run any code until the caller starts enumerating via MoveNext().
- Naive argument validation inside an iterator block is itself deferred, surprising callers who expect immediate errors.
- Split validation into an eager public wrapper and a private iterator method to fix deferred-validation surprises.
- This public/private split pattern mirrors how the .NET team implements built-in operators like Where and Select.
- Well-behaved custom operators compose fluently with built-in ones, e.g. source.Where(...).Batch(50).
Practice what you learned
1. What is the required first parameter signature for a custom LINQ extension method?
2. What C# keyword is typically used to implement deferred execution in a custom LINQ operator?
3. Why can placing argument validation directly inside a yield return iterator block be problematic?
4. What is the standard fix for the deferred-validation problem in custom LINQ iterator methods?
5. Which .NET built-in operators follow the same public/private split pattern described for custom operators?
Was this page helpful?
You May Also Like
Lambda Expressions in LINQ
Understand how lambda expressions power LINQ's filtering, projection, and aggregation operators, and how they differ from anonymous methods and expression trees.
Expression Trees Explained
Learn what expression trees are, how the compiler builds them from lambdas, and how providers like EF Core walk them to generate SQL.
Parallel LINQ (PLINQ)
Learn how PLINQ parallelizes LINQ to Objects queries across multiple cores using AsParallel, and when parallelism actually helps.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics