How to Chain Lambda Expressions in C
Chaining lambda expressions in C# allows developers to write modular,
readable, and highly reusable functional code. This article covers the
fundamental approaches to combining lambdas, starting with standard LINQ
method chaining, moving to combining Func<T, bool>
delegates with custom extension methods, and finally composing
Expression<Func<T, bool>> trees for database
queries in Entity Framework.
1. Built-In LINQ Method Chaining
The most common way to chain lambda expressions is through standard
LINQ extension methods provided in the System.Linq
namespace. Each method accepts a lambda and returns an
IEnumerable<T> or IQueryable<T>,
allowing successive calls.
using System;
using System.Collections.Generic;
using System.Linq;
var numbers = Enumerable.Range(1, 20);
var result = numbers
.Where(x => x % 2 == 0) // First lambda
.Where(x => x > 10) // Second lambda chained
.Select(x => x * x); // Transformation lambda
Console.WriteLine(string.Join(", ", result)); // Output: 144, 196, 256, 324, 4002. Chaining
In-Memory Predicates (Func<T, bool>)
When you need to dynamically combine multiple filter conditions in
memory, you can create extension methods that logically combine
delegates using AND or OR logic.
The Extension Methods
using System;
public static class PredicateExtensions
{
public static Func<T, bool> And<T>(this Func<T, bool> left, Func<T, bool> right)
{
return x => left(x) && right(x);
}
public static Func<T, bool> Or<T>(this Func<T, bool> left, Func<T, bool> right)
{
return x => left(x) || right(x);
}
}Usage Example
Func<int, bool> isEven = x => x % 2 == 0;
Func<int, bool> isGreaterThanTen = x => x > 10;
// Chain the lambdas into a single delegate
Func<int, bool> isEvenAndGreaterThanTen = isEven.And(isGreaterThanTen);
var numbers = new[] { 4, 8, 12, 15, 18 };
var filtered = numbers.Where(isEvenAndGreaterThanTen);
// Output: 12, 183. Function Composition (Pipelining Transformations)
To chain transformation functions (Func<T, T>)
where the output of one lambda serves as the input to the next, define a
Pipe or Compose extension method.
public static class FunctionExtensions
{
public static Func<T, T> Pipe<T>(this Func<T, T> first, Func<T, T> second)
{
return x => second(first(x));
}
}
// Usage
Func<string, string> trim = s => s.Trim();
Func<string, string> toUpper = s => s.ToUpper();
Func<string, string> addBrackets = s => $"[{s}]";
// Chain the operations together
var formatText = trim.Pipe(toUpper).Pipe(addBrackets);
string output = formatText(" hello world ");
// Output: "[HELLO WORLD]"4.
Chaining Expression Trees for IQueryable (Entity
Framework)
When querying databases with Entity Framework, passing a raw delegate
will cause queries to evaluate client-side. Instead, you must combine
Expression<Func<T, bool>> objects by rewriting
parameter references using an ExpressionVisitor.
The Parameter Rebinder Visitor
using System;
using System.Linq.Expressions;
public class ParameterReplacer : ExpressionVisitor
{
private readonly ParameterExpression _oldParam;
private readonly ParameterExpression _newParam;
public ParameterReplacer(ParameterExpression oldParam, ParameterExpression newParam)
{
_oldParam = oldParam;
_newParam = newParam;
}
protected override Expression VisitParameter(ParameterExpression node)
{
return node == _oldParam ? _newParam : base.VisitParameter(node);
}
}
public static class ExpressionExtensions
{
public static Expression<Func<T, bool>> AndAlso<T>(
this Expression<Func<T, bool>> left,
Expression<Func<T, bool>> right)
{
var parameter = Expression.Parameter(typeof(T), "x");
var leftVisitor = new ParameterReplacer(left.Parameters[0], parameter);
var leftBody = leftVisitor.Visit(left.Body);
var rightVisitor = new ParameterReplacer(right.Parameters[0], parameter);
var rightBody = rightVisitor.Visit(right.Body);
var combinedBody = Expression.AndAlso(leftBody!, rightBody!);
return Expression.Lambda<Func<T, bool>>(combinedBody, parameter);
}
}Usage with EF Core or LINQ to SQL
Expression<Func<Product, bool>> inStock = p => p.UnitsInStock > 0;
Expression<Func<Product, bool>> affordable = p => p.Price < 50.0m;
// Combine expression trees into a single SQL-translatable expression
var filter = inStock.AndAlso(affordable);
// Database execution:
// SELECT * FROM Products WHERE UnitsInStock > 0 AND Price < 50.0
var query = dbContext.Products.Where(filter).ToList();