🔧 Mastering Action and Func Delegates in C#: Real-World Patterns and Examples
In C#, delegates like Action and Func allow you to treat behavior as data — meaning you can pass methods around like variables.
This concept is powerful and opens doors to cleaner, more flexible, and decoupled code.
🧠 In This Article:
What Action and Func are
How to use them effectively
Real-world examples using design patterns like Strategy, Command, and Response pipelines
🔹 What Are Action and Func in C#?
✅ Action Delegate
Represents a method that returns void and can take 0 to 16 parameters.
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("Alice"); // Output: Hello, Alice!
✅ Func Delegate
Represents a method that returns a value, with 0 to 16 parameters (last one is the return type).
Func<int, int, int> add = (a, b) => a + b;
int sum = add(3, 4); // sum = 7
💡 Why Use Action and Func?
Decouple logic from implementation
Enable cleaner LINQ, event handling, callbacks, and dynamic behavior
Help implement design patterns with less boilerplate
🎯 Real-World Example 1: Strategy Pattern Using Func
The Strategy Pattern allows you to swap algorithms at runtime. Here’s how it looks using Func:
public class TaxCalculator
{
private readonly Func<decimal, decimal> _taxStrategy;
public TaxCalculator(Func<decimal, decimal> taxStrategy)
{
_taxStrategy = taxStrategy;
}
public decimal Calculate(decimal amount) => _taxStrategy(amount);
}
✅ Usage:
var standardTax = new TaxCalculator(amount => amount * 0.2m);
Console.WriteLine(standardTax.Calculate(100)); // Output: 20
var reducedTax = new TaxCalculator(amount => amount * 0.1m);
Console.WriteLine(reducedTax.Calculate(100)); // Output: 10
✔ Clean and flexible, no need for extra strategy classes.
🎮 Real-World Example 2: Command Pattern Using Action
Encapsulate requests using Action, like a simple remote control:
public class RemoteControl
{
private readonly Action _command;
public RemoteControl(Action command)
{
_command = command;
}
public void PressButton() => _command();
}
✅ Usage:
var turnOnLight = new RemoteControl(() => Console.WriteLine("Light On"));
turnOnLight.PressButton(); // Output: Light On
var playMusic = new RemoteControl(() => Console.WriteLine("Music Playing"));
playMusic.PressButton(); // Output: Music Playing
🔁 Combining Action and Func: Workflow Example
Simulate a simple data pipeline using both Func and Action:
Func<string> fetchData = () => "raw data";
Func<string, string> processData = data => data.ToUpper();
Action<string> saveData = result => Console.WriteLine($"Saved: {result}");
var raw = fetchData();
var processed = processData(raw);
saveData(processed); // Output: Saved: RAW DATA
📌 Chaining Operations with Func and Extension Methods
You can build your own functional pipelines using extension methods:
public static class Extensions
{
public static T PerformOperation<T>(this T value, Func<T, T> performer) where T : struct
{
return performer(value);
}
}
✅ Usage:
public void DoWorkWithStandardLambda()
{
Func<int, int> toPowerFour = x => x * x * x * x;
Func<int, int> makeNegative = x => -1 * x;
int value = 5;
int resultA = value
.PerformOperation(toPowerFour)
.PerformOperation(makeNegative)
.PerformOperation(x => x + 10); // Final result: -615
double doubleVal = 7.5;
double resultB = doubleVal
.PerformOperation(x => Math.Sin(x))
.PerformOperation(x => x * Math.PI);
}
🔄 Functional Pipelines with a Response Pattern
You can adapt PerformOperation to work with a Result wrapper for success/failure chaining:
🧱 Result Class
public class Result<T>
{
public bool IsSuccess { get; }
public T Value { get; }
public string? Error { get; }
public Result(T value) => (IsSuccess, Value) = (true, value);
public Result(string error) => (IsSuccess, Error) = (false, error);
}
🔧 Chained Perform for Result:
public static class ResultExtensions
{
public static Result<T> Perform<T>(this Result<T> result, Func<T, T> func)
{
return result.IsSuccess
? new Result<T>(func(result.Value))
: result;
}
}
✅ Usage:
var result = new Result<int>(5)
.Perform(x => x * x)
.Perform(x => x + 10); // Final value: 35
If there's an error, the chain short-circuits:
var errorResult = new Result<int>("Invalid input")
.Perform(x => x * x); // Skipped
🧠 Final Thoughts
Understanding and leveraging Action and Func can radically simplify your C# code. From dynamic strategies to functional pipelines, they help you express logic clearly and flexibly — without drowning in boilerplate.
Before you create another class just to pass a behavior, ask yourself:
“Can I just use a delegate instead?”
References
Top comments (2)
hey, thanks for sharing, you can also use CSharp syntax highlighting this way:
Great, thanks! Unfortunately, I'm not very good at formatting.
I’ll keep it in mind for next time.