Delegates, Events & Async
Why This Matters
Delegates, events, and async/await are the backbone of modern C# applications. They let you write code that is flexible, responsive, and scalable. Delegates allow methods to be passed as parameters — like handing a task to someone else. Events enable your app to react to user actions or system changes — think of clicking a button. Async/await keeps your app fast and responsive by letting it do other work while waiting for long operations (like downloading data). Mastering these will make you a true C# expert!
Delegates – The Method Pass
A delegate is a type that holds a reference to a method. It's like a remote control for functions.
// 1. Declare a delegate (a blueprint for methods)
public delegate int Operation(int a, int b);
// 2. Create methods that match the blueprint
public int Add(int a, int b) => a + b;
public int Multiply(int a, int b) => a * b;
// 3. Use the delegate to point to different methods
Operation op = Add;
int result = op(5, 3); // result = 8
op = Multiply;
result = op(5, 3); // result = 15
What's happening here?
- Step 1: We define a delegate called
Operationthat can point to any method taking two integers and returning an integer. - Step 2: We create two methods (
AddandMultiply) that match this signature. - Step 3: We assign different methods to the delegate variable and call it. This lets us change behavior at runtime - very powerful!
Real-world use: Calculator apps, sorting algorithms (where you can swap comparison logic), and plugin systems.
// Func – returns a value (last type is the return type)
Func<int, int, int> add = (a, b) => a + b;
int sum = add(5, 3); // sum = 8
// Action – no return value (just does something)
Action<string> print = (msg) => Console.WriteLine(msg);
print("Hello World!");
Why use Func and Action?
- Func is for methods that return a value. The last type parameter is always the return type.
- Action is for methods that don't return anything (void).
- They save you from declaring custom delegates. C# provides them out-of-the-box!
When to use: Use Func for calculations, transformations, or any operation that produces a result. Use Action for logging, UI updates, or side-effect operations.
Events – Notify When Something Happens
An event is a special delegate that lets objects broadcast when something occurs. Other objects can subscribe to react.
public class Button
{
// Define the event (using EventHandler is standard)
public event EventHandler Clicked;
public void Click()
{
// Raise the event – notify all subscribers
Clicked?.Invoke(this, EventArgs.Empty);
}
}
// Usage:
Button button = new Button();
button.Clicked += (sender, e) => Console.WriteLine("Button clicked!");
button.Click(); // Output: Button clicked!
How events work:
- Publisher: The
Buttonclass defines the event and raises it when something happens. - Subscriber: Your code "subscribes" to the event using
+=and provides a method to run when the event fires. - Safety: The
?.Invokechecks if anyone is subscribed before raising the event (prevents null reference errors).
Why events are powerful: They decouple the sender (button) from the responder (your code). The button doesn't need to know what will happen when clicked - it just notifies. This makes your code more modular and maintainable. Used everywhere in UI frameworks, game development, and system notifications.
Async/Await – Keep Your App Responsive
Async/await lets your app do other work while waiting for slow tasks (like web requests or file I/O). It's the most important feature for building modern, scalable applications.
Understanding the Concept
Imagine you're cooking dinner. You put the pasta in boiling water (a long task). Instead of standing there staring at the pot, you chop vegetables, set the table, and check your phone. When the pasta is done, you come back to it. That's exactly how async/await works!
- Synchronous (blocking): You stare at the pot until the pasta is done. Your app freezes.
- Asynchronous (non-blocking): You do other productive work while waiting. Your app stays responsive.
// Basic structure of an async method
public async Task<string> FetchDataAsync()
{
// Simulate a 2-second network call
await Task.Delay(2000);
return "Data loaded!";
}
// Calling it:
public async Task ProcessAsync()
{
string result = await FetchDataAsync();
Console.WriteLine(result);
}
Breaking Down the Keywords:
- async: This keyword marks the method as asynchronous. It tells the compiler "this method contains await operations."
- Task<T>: Represents an operation that will return a value of type T in the future. If your method returns nothing, use
Task(notvoid). - await: This is where the magic happens! It pauses the method without blocking the thread. While waiting, the thread can go do other work (like handling UI clicks or processing other requests).
- Return: When the awaited task completes, the method resumes right after the await, with the result ready to use.
⚠️ Important: Never use async void except for event handlers! Always return Task or Task<T> so callers can await your method and handle exceptions properly.
The State Machine
When you write async code, the C# compiler transforms your method into a state machine. This is what happens:
- Method Start: The method runs synchronously until it hits the first
await. - Await Encountered: The method returns a
Taskto the caller (who can continue with other work). - Waiting: The awaited operation starts. The thread is freed to do other things.
- Completion: When the operation finishes, the state machine resumes the method from where it paused.
- Continuation: The method continues executing after the await, eventually returning the result.
Key insight: Async/await doesn't create new threads! It uses the existing thread pool efficiently. For I/O operations (web requests, database queries), it's thread-free during the wait.
// Visual representation of what happens:
// The compiler transforms your code into something like this:
public Task<string> FetchDataAsync()
{
var stateMachine = new StateMachine();
stateMachine.MoveNext();
return stateMachine.Task;
}
private class StateMachine
{
private int state = 0;
private TaskAwaiter awaiter;
public TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
public Task<string> Task => tcs.Task;
public void MoveNext()
{
switch(state)
{
case 0:
awaiter = Task.Delay(2000).GetAwaiter();
if (!awaiter.IsCompleted)
{
state = 1;
awaiter.OnCompleted(MoveNext);
return;
}
goto case 1;
case 1:
awaiter.GetResult();
tcs.SetResult("Data loaded!");
break;
}
}
}
using HttpClient client = new HttpClient();
public async Task<string> GetWebContentAsync(string url)
{
try
{
// This starts the network request and immediately returns control
HttpResponseMessage response = await client.GetAsync(url);
// Check if the request succeeded
response.EnsureSuccessStatusCode();
// Read the response body asynchronously
string content = await response.Content.ReadAsStringAsync();
return content;
}
catch (HttpRequestException ex)
{
return $"Error: {ex.Message}";
}
catch (TaskCanceledException)
{
return "Request timed out!";
}
}
What's happening in this real-world example:
- GetAsync: Starts a network request. This could take 1-10 seconds. The await releases the thread while waiting for the server response.
- EnsureSuccessStatusCode: Throws an exception for HTTP errors (404, 500, etc.) so you can handle them gracefully.
- ReadAsStringAsync: Reads the response body - also asynchronous because it might be large and require multiple network packets.
- Exception Handling: Catch specific exceptions to provide meaningful error messages to users.
Pro tip: Always use HttpClient as a static or singleton instance. Creating a new one for each request can cause socket exhaustion!
// Scenario: Fetch data from three different APIs
public async Task ProcessAllAsync()
{
// Start ALL tasks at the same time (they run in parallel)
Task<string> task1 = FetchUserDataAsync();
Task<string> task2 = FetchProductDataAsync();
Task<string> task3 = FetchOrderDataAsync();
// Wait for ALL to complete
string[] results = await Task.WhenAll(task1, task2, task3);
// All data is ready to use
Console.WriteLine($"User: {results[0]}");
Console.WriteLine($"Products: {results[1]}");
Console.WriteLine($"Orders: {results[2]}");
}
// Alternative: Process tasks one by one (slower)
public async Task ProcessSequentialAsync()
{
string user = await FetchUserDataAsync(); // Wait 3 seconds
string products = await FetchProductDataAsync(); // Wait 2 more seconds
string orders = await FetchOrderDataAsync(); // Wait 1 more second
// Total: 6 seconds
}
// Parallel version: All start at the same time
// Total: Only 3 seconds (the longest one)!
Why parallel execution is a game-changer:
- Task.WhenAll starts multiple tasks simultaneously, not one after another.
- If each fetch takes 2 seconds, sequential would take 6 seconds, but parallel takes only 2 seconds! That's 3x faster.
- The await waits for ALL tasks to complete before continuing.
- Perfect for loading dashboards, reports, or any page that aggregates data from multiple sources.
Performance boost: Use this pattern whenever you have independent operations that don't depend on each other's results. This is how modern websites load so fast!
// Using CancellationToken to cancel long-running operations
public async Task<string> FetchWithTimeoutAsync(
string url,
CancellationToken cancellationToken = default)
{
using (CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token))
{
try
{
HttpResponseMessage response = await client.GetAsync(url, cts.Token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (OperationCanceledException)
{
return "Request was cancelled or timed out!";
}
}
}
// Usage with cancellation
public async Task ExampleUsageAsync()
{
// Create a cancellation token that cancels after 3 seconds
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
string result = await FetchWithTimeoutAsync("https://api.example.com/data", cts.Token);
Console.WriteLine(result);
}
Why cancellation matters:
- User experience: Users can cancel long operations (like downloading a large file).
- Resource management: Prevent wasted resources on operations that are no longer needed.
- Timeouts: Automatically cancel operations that take too long (5 seconds is a good default for web APIs).
- Graceful degradation: Provide fallback behavior when operations fail or timeout.
Best practice: Always provide cancellation token support in your async methods. Your users will thank you!
Golden Rules of Async/Await:
async Task for async methods
await with Task.WhenAll for parallel operations
async void (except event handlers)
.Result or .Wait() - they can deadlock!
Task.Run - only for CPU-bound work
// ❌ BAD: Blocking on async code (causes deadlocks!)
public void BadExample()
{
var result = FetchDataAsync().Result; // This will deadlock in UI apps!
}
// ✅ GOOD: Use async all the way down
public async Task GoodExample()
{
var result = await FetchDataAsync(); // Proper async handling
}
// ❌ BAD: Async void (exceptions are lost, hard to test)
public async void BadEventHandler()
{
await SomeAsyncOperation(); // If this throws, your app crashes without warning
}
// ✅ GOOD: Return Task from event handlers
public async Task GoodEventHandler()
{
await SomeAsyncOperation(); // Properly handled
}
Key Takeaway
Delegates = method references (like assigning a task).
Events = notifications (like a alarm bell).
Async/await = non-blocking operations (keep UI responsive, scale web apps).
Master these and you'll write cleaner, faster, and more maintainable C# code.
Task:
Create a WeatherStation class that:
- Has a
TemperatureChangedevent (useEventHandler<int>). - Has a
SetTemperature(int temp)method that raises the event. - In
Main, subscribe to the event and print "Temperature is now {temp}°C". - Also use a
Func<int, int>delegate to convert Celsius to Fahrenheit (formula: F = C * 9/5 + 32).
Hint: Use the code snippets above as reference. Write the solution, then click below to check.