Skip to Content
Course content

49: Func, Action, and Predicate Delegates

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a junior dev who was building a report generator. He had created about fifteen different methods that all looked almost identical: FilterOrdersByDate, FilterOrdersByCustomer, FilterOrdersByAmount, and so on. Each method took a list of orders and returned a filtered list. It was a maintenance nightmare. Every time a stakeholder wanted a new filter, he had to write a whole new method, compile, and deploy. I told him, "Stop writing methods that only differ by one line of logic. Just pass the logic itself as a parameter."

That's where these built-in delegate types come in. You already know how to declare a custom delegate, but in modern C#, we almost never do that manually. It's too much boilerplate. Microsoft gave us Action, Func, and Predicate to handle 99% of our needs. Think of them as generic templates for function pointers.

The Three Workhorses of Logic Passing

First, let's look at Action. An Action is for when you want to do something, but you don't need a value back. It's a "fire and forget" delegate. It returns void. You can have an Action with no parameters, or one that takes up to 16 different arguments. I use these all the time for logging or updating a UI progress bar where the method performing the work doesn't care what happens after the update is sent.

// An Action that takes a string and returns void
Action<string> logger = message => Console.WriteLine($"[LOG]: {message}");
logger("System started"); 

Then we have Func. This is the opposite of Action; it must return a value. The magic of Func is in the generic parameters. The last type listed is always the return type. So, Func<int, int, string> takes two integers and returns a string. If you see a Func, think "calculation" or "transformation."

// Takes two ints, returns an int
Func<int, int, int> add = (a, b) => a + b;
int result = add(5, 10); // 15 

Finally, there's Predicate<T>. Now, technically, a Predicate<T> is just a specialized version of Func<T, bool>. It takes one object and returns a boolean. Why does it exist separately? Mostly for semantic clarity. When you see Predicate in a method signature, you know instantly that the method is testing whether an object meets a certain condition. It's the backbone of most filtering logic in .NET.

Integrating Delegates into Your Classes

The real power happens when you stop using these as local variables and start using them as parameters for your methods. This allows you to write "Higher-Order Functions"—methods that don't know how they'll be processing data, only that they will process it using the logic you provide.

Imagine a DiscountEngine. Instead of hard-coding a "Black Friday" or "Clearance" discount, you can just let the caller pass in the math.

public class DiscountEngine
{
    public decimal ApplyDiscount(decimal price, Func<decimal, decimal> discountStrategy)
    {
        return discountStrategy(price);
    }
}

// Usage
var engine = new DiscountEngine();

// Pass a 10% discount logic on the fly
var tenPercent = engine.ApplyDiscount(100m, p => p * 0.9m);

// Pass a flat $20 off logic
var flatDiscount = engine.ApplyDiscount(100m, p => p - 20m); 

By using Func here, the DiscountEngine is now completely decoupled from the business rules. You can add a thousand different discount types without ever touching the DiscountEngine class again. That's how you avoid the "method explosion" I mentioned earlier.




📋 Practical Task

Build a Dynamic Inventory Filter and Notifier

You are tasked with creating a simple inventory management utility. You need to build a system that can filter a list of products based on a dynamic condition and then execute a custom action for every product that matches that condition.

Requirements:

  • Create a Product class with properties for Name (string), Price (decimal), and StockQuantity (int).
  • Create an InventoryManager class with a method called ProcessProducts.
  • The ProcessProducts method must take three parameters:
    1. A List<Product>.
    2. A Predicate<Product> to determine which products to process.
    3. An Action<Product> to define what to do with each product that passes the filter.
  • In your Main method, instantiate a list of at least 5 products.
  • Call ProcessProducts twice:
    1. First, filter for products with StockQuantity less than 5 and use the Action to print "Restock needed for: [Name]".
    2. Second, filter for products with Price greater than 100 and use the Action to print "Premium item found: [Name]".
Rating
0 0

There are no comments for now.

to be the first to leave a comment.