C#  

Understanding Predicate, Anonymous Methods, and Lambda Expressions in C#

Predicate

Say you have an HR portal with the following Employee class:

public class Employee
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Suppose the HR manager needs to retrieve employees who are older than 30 years. You might create the following method:

public static List<Employee> GetEmployeesOlderThan30(List<Employee> employees)
{
    List<Employee> result = new();

    foreach (var employee in employees)
    {
        if (employee.Age > 30)
        {
            result.Add(employee);
        }
    }

    return result;
}

In the main program:

var result = GetEmployeesOlderThan30(employees);

Everything works perfectly. However, business requirements change frequently. The HR manager might later request:

  • Employees older than 28

  • Employees whose names start with C

  • Employees between ages 35 and 40

Soon, your code starts looking like this:

GetEmployeesOlderThan28();

GetEmployeesOlderThan40();

GetEmployeesInBetween35To40();

GetEmployeesNameStartsWithC();

GetEmployeesNameContainsJohn();

If we look back at our GetEmployeesOlderThan30() method, only this line changes:

if (employee.Age > 30)

Everything else is repeated:

  • foreach

  • Create List

  • Add()

  • Return List

Instead of creating multiple methods that differ only by the filtering condition, we can use a predicate and pass the condition into the method.

Imagine a simple variable declaration:

int number = 5;

The variable number stores the value 5.

With delegates, instead of storing a value, we store a method:

SomeDelegate filter = SomeMethod;

Let's return to our employee filtering example. First, we create a delegate called EmployeeFilter that accepts an Employee.

public delegate bool EmployeeFilter(Employee employee);

Then we write a reusable filtering method instead of hardcoding logic such as "older than 30" or "name starts with C".

public static List<Employee> FilterEmployees(
    List<Employee> employees,
    EmployeeFilter filter)
{
    List<Employee> result = new();

    foreach (var employee in employees)
    {
        if (filter(employee)) // Dynamic filtering logic
        {
            result.Add(employee);
        }
    }

    return result;
}

Now the method contains no fixed business logic. The filtering logic becomes something you pass in, just like a variable.

For example, to check whether an employee is older than 30:

public static bool IsOlderThan30(Employee employee)
{
    return employee.Age > 30;
}

Or to check whether the employee's name starts with A:

public static bool NameStartsWithA(Employee employee)
{
    return employee.Name.StartsWith("A");
}

In the main program:

var olderEmployees =
    FilterEmployees(employees, IsOlderThan30);

var employeesStartsWithA =
    FilterEmployees(employees, NameStartsWithA);

Notice that FilterEmployees receives method names (IsOlderThan30 and NameStartsWithA) as parameters.

Delegates allow us to pass the filtering condition as a parameter, making the filtering method reusable.

Generic Predicate

Now suppose we don't just have an Employee class. We also have Product, Customer, Order, and many others.

We don't want to create a different delegate for every class.

public delegate bool ProductFilter(Product product);
public delegate bool OrderFilter(Order order);
public delegate bool CustomerFilter(Customer customer);

Nor do we want to duplicate filtering methods:

FilterProducts(...);
FilterOrders(...);
FilterCustomers(...);

Instead, we can use a generic predicate.

public delegate bool Predicate<T>(T obj);

Then create a generic filtering method.

public static List<T> Filter<T>(
    List<T> items,
    Predicate<T> predicate)
{
    List<T> result = new();

    foreach (var item in items)
    {
        if (predicate(item))
        {
            result.Add(item);
        }
    }

    return result;
}

The implementation now becomes:

var olderEmployees = Filter(employees, IsOlderThan30);

var youngEmployees = Filter(employees, IsYoungerThan25);

var nameStartsWithA = Filter(employees, NameStartsWithA);

var expensiveProducts = Filter(products, IsPriceOver3000);

var cheapProducts = Filter(products, p => IsPriceUnder100);

var expirySoon = Filter(products, p => IsExpire3Months);

You may argue that we still need to maintain methods such as IsOlderThan30(), NameStartsWithA(), and IsPriceUnder100(). So what is the point of using a generic predicate?

Although the filtering logic is different, every filtering method previously repeated the same pattern:

  • Create a list

  • Loop through the collection

  • Evaluate a condition

  • Add matching items

  • Return the list

Using a generic predicate removes that repetition and improves maintainability.

It also allows each domain to control its own business logic. Employee-related logic stays within the employee domain, while order-related logic remains within the order domain.

Because predicates become modular "logic blocks," they can easily be combined.

For example, suppose HR needs employees whose names start with A and whose age is below 40.

var result = Filter(employeeList, e =>
    IsNameStartWithA(e) && IsYoungerThan40(e));

Without predicates, you would have to create another filtering method.

foreach (var employee in employees)
{
    if (employee.Age > 30 && employee.Name.StartsWith("A"))
    {
        result.Add(employee);
    }
}

Anonymous Methods & Lambda Expressions

Let's return to our method that checks whether an employee is older than 30.

public static bool IsAgeOver30(Employee employee)
{
    return employee.Age > 30;
}

This method works perfectly. However, if it is only used once, creating an entire method for a single line of logic feels unnecessary.

This is where anonymous methods become useful.

Previously, we passed the method name:

ShowEmployees(employees, IsAgeOver30);

Instead, we can place the entire method body directly where it is needed.

ShowEmployees(
    employees,
    delegate(Employee employee)
    {
        return employee.Age > 30;
    });

It has no method name, which is why it is called an anonymous method.

Compare it with our delegate declaration:

delegate bool EmployeeFilter(Employee employee);

Both have:

  • Parameter: Employee

  • Return type: bool

Therefore, they are compatible.

Using anonymous methods reduces the need for small helper methods.

Later, the C# language designers introduced lambda expressions. Since much of the type information can be inferred from the target delegate or expression context, the syntax becomes shorter and easier to read.

This anonymous method:

delegate(Employee employee)
{
    return employee.Age > 30;
}

Can be simplified to:

employee => employee.Age > 30

The code then becomes:

ShowEmployees(
    employees,
    employee => employee.Age > 30);

Lambda expressions can also contain multiple statements.

Anonymous method:

delegate(Employee employee)
{
    Console.WriteLine(employee.Name);

    return employee.Age > 30;
}

Equivalent lambda expression:

employee =>
{
    Console.WriteLine(employee.Name);

    return employee.Age > 30;
}

Predicate, anonymous methods, and lambda expressions are more than just language syntax—they represent a shift toward writing reusable, maintainable, and expressive code. Instead of creating countless methods for every business requirement, we can separate the filtering logic from the filtering process and compose behavior as needed.

Summary

Predicates separate filtering logic from filtering implementation, making code more reusable and maintainable. Combined with generic predicates, anonymous methods, and lambda expressions, they help eliminate repetitive code, keep business logic modular, and make applications easier to extend as requirements evolve.