Custom Extension Methods for C# LINQ Queries
Custom extension methods in C# allow developers to extend LINQ’s
querying capabilities, improve code readability, and encapsulate
reusable data-transformation logic. This article explains how to create
your own LINQ operators targeting IEnumerable<T>,
detailing the necessary syntax, the role of static classes, and how to
maintain deferred execution using the yield return
keyword.
Prerequisites for Creating an Extension Method
To extend LINQ, your method must satisfy three fundamental C# rules:
- It must be defined inside a non-nested, non-generic static class.
- The method itself must be declared as static.
- The first parameter must include the
thiskeyword, specifying the type being extended (typicallyIEnumerable<T>for LINQ queries).
Implementing
Deferred Execution with yield return
Standard LINQ operators like Where and
Select use deferred execution, meaning the query does not
execute until the data is enumerated (e.g., inside a
foreach loop or by calling .ToList()). Custom
LINQ methods should mirror this behavior using
yield return.
Here is an implementation of a custom operator called
TakeEvery, which returns every N-th element from a
sequence:
using System;
using System.Collections.Generic;
public static class LinqExtensions
{
public static IEnumerable<T> TakeEvery<T>(this IEnumerable<T> source, int step)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
if (step <= 0)
{
throw new ArgumentOutOfRangeException(nameof(step), "Step must be greater than zero.");
}
return Iterator();
// Local function ensures argument validation happens immediately,
// while enumeration remains deferred.
IEnumerable<T> Iterator()
{
int index = 0;
foreach (T item in source)
{
if (index % step == 0)
{
yield return item;
}
index++;
}
}
}
}Creating Conditional Query Extensions
Another common use case is conditional filtering, which avoids
breaking up fluent LINQ query chains with external if
statements. The following WhereIf method applies a filter
only when a specified condition is true:
using System;
using System.Collections.Generic;
using System.Linq;
public static class QueryableExtensions
{
public static IEnumerable<T> WhereIf<T>(
this IEnumerable<T> source,
bool condition,
Func<T, bool> predicate)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (predicate == null) throw new ArgumentNullException(nameof(predicate));
return condition ? source.Where(predicate) : source;
}
}Using the Custom Methods in Queries
Once defined within an imported namespace, custom methods seamlessly integrate with existing LINQ operators:
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var numbers = Enumerable.Range(1, 20);
bool filterOnlyLarge = true;
var result = numbers
.WhereIf(filterOnlyLarge, n => n > 5)
.TakeEvery(2)
.ToList();
Console.WriteLine(string.Join(", ", result));
// Output: 6, 8, 10, 12, 14, 16, 18, 20
}
}Supporting
IQueryable<T> for Database Providers
If you need to execute custom methods against database contexts (such
as Entity Framework Core), extend IQueryable<T>
instead of IEnumerable<T>. This requires manipulating
expression trees so the underlying provider can translate your logic
into SQL:
using System;
using System.Linq;
using System.Linq.Expressions;
public static class DatabaseLinqExtensions
{
public static IQueryable<T> WhereIf<T>(
this IQueryable<T> source,
bool condition,
Expression<Func<T, bool>> predicate)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (predicate == null) throw new ArgumentNullException(nameof(predicate));
return condition ? source.Where(predicate) : source;
}
}Best Practices
- Separate Validation from Iteration: Throw argument exceptions immediately before yielding execution to avoid delayed runtime failures.
- Avoid Premature Materialization: Do not call
.ToList()or.ToArray()inside intermediate LINQ operators; stream the data usingyield returnor return chainedIEnumerable<T>references. - Match Namespaces: Place generic LINQ extensions
either in
System.Linqor a root utility namespace so they are readily available without requiring nicheusingdirectives.