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, 400

2. 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, 18

3. 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();