PYnative

Python Programming

  • Learn Python ▼
    • Python Tutorials
    • Python Basics
    • Python Interview Q&As
  • Exercises ▼
    • Python Exercises
    • C++ Exercises
    • C Programming Exercises
    • Java Exercises
    • C# Exercises
  • Quizzes
  • Code Editor ▼
    • Online Python Code Editor
    • Online C Compiler
    • Online C++ Compiler
    • Online Java Compiler
    • Online C# Compiler
Home » C# Exercises » C# Delegates and Events Exercises: 20 Coding Problems with Solutions

C# Delegates and Events Exercises: 20 Coding Problems with Solutions

Updated on: July 15, 2026 | Leave a Comment

Delegates and events are what make C# capable of flexible, decoupled designs, from simple callbacks to full publisher-subscriber systems. This collection of 21 C# exercises covers declaring and invoking custom delegates, chaining multicast delegates, and building event-driven code using C#’s event keyword.

Each exercise includes a Practice Problem, Purpose, Hint, and a full Solution with Explanation, so you understand how delegates and events fit together in real applications.

Also, See: C# Exercises with  22 topic-wise sets and 620+ practice questions.

What You’ll Practice

  • Delegates: Declaring, assigning, and invoking custom delegate types.
  • Multicast Delegates: Chaining multiple methods to a single delegate.
  • Events: Publisher-subscriber patterns using the event keyword.
  • Built-in Delegates: Func<T>, Action<T>, and EventHandler.
+ Table Of Contents (21 Exercises)

Table of contents

  • Exercise 1: The Math Operator
  • Exercise 2: String Transformer
  • Exercise 3: Filter List of Integers
  • Exercise 4: Multicast Logger
  • Exercise 5: Anonymous Greeting
  • Exercise 6: Lambda Conversion
  • Exercise 7: Action Reporter
  • Exercise 8: Func Calculator
  • Exercise 9: Predicate Matcher
  • Exercise 10: The Button Click
  • Exercise 11: Stock Price Tracker
  • Exercise 12: Thermostat Alert
  • Exercise 13: Unsubscribing Memory Leak
  • Exercise 14: Standard .NET Event Pattern
  • Exercise 15: Safe Event Invocation
  • Exercise 16: Text File Downloader
  • Exercise 17: Generic Data Pipeline
  • Exercise 18: BankAccount Overdraft Protection
  • Exercise 19: The Chat Room Broadcast
  • Exercise 20: Traffic Light System
  • Exercise 21: Custom Event Accessors (add / remove)

Exercise 1: The Math Operator

Practice Problem: Create a delegate called MathOperation that accepts two integers and returns an integer. Implement Add and Multiply methods, then assign them to the delegate to perform calculations.

Purpose: This exercise helps you practice declaring a delegate type and understand how delegates let you hold a reference to a method and invoke it indirectly, the foundation of callback patterns and event handling in C#.

Given Input: a = 10, b = 5, invoked once through Add and once through Multiply.

Expected Output:

Add Result = 15
Multiply Result = 50
▼ Hint
  • Declare a delegate signature that matches Add and Multiply: two int parameters returning an int.
  • Write the Add and Multiply methods so their signatures match the delegate exactly.
  • Assign the delegate variable to Add first, invoke it, then reassign it to Multiply and invoke again.
  • Remember that a delegate variable can point to any method as long as the signature matches.
▼ Solution & Explanation
using System;

delegate int MathOperation(int a, int b);

class Program
{
    static int Add(int a, int b) => a + b;
    static int Multiply(int a, int b) => a * b;

    static void Main()
    {
        MathOperation operation = Add;
        Console.WriteLine("Add Result = " + operation(10, 5));

        operation = Multiply;
        Console.WriteLine("Multiply Result = " + operation(10, 5));
    }
}Code language: C# (cs)

Explanation:

  • delegate int MathOperation(int a, int b): Declares a delegate type that can reference any method taking two integers and returning an integer.
  • MathOperation operation = Add: Assigns the Add method to the delegate variable without calling it yet.
  • operation(10, 5): Invokes whichever method the delegate currently points to, using the same call syntax as a normal method call.
  • operation = Multiply: Reassigns the delegate to a different method, showing that the same variable can represent different behavior at runtime.

Exercise 2: String Transformer

Practice Problem: Write a delegate StringTransformer that takes a string and returns a string. Pass it to a method that processes a list of names, letting the caller choose whether to convert them to uppercase, lowercase, or reverse them.

Purpose: This exercise helps you practice passing delegates as method parameters, which decouples the transformation logic from the method that applies it, a pattern used heavily in LINQ and functional-style C# code.

Given Input: names = ["Alice", "Bob", "Charlie"], processed with an uppercase transformer.

Expected Output:

ALICE
BOB
CHARLIE
▼ Hint
  • Declare the delegate as string StringTransformer(string input).
  • Write separate methods for uppercase, lowercase, and reverse, each matching that signature.
  • Create a ProcessNames method that accepts a list of names and a StringTransformer delegate, then loops through the list applying the delegate to each item.
  • Call ProcessNames with different methods to see the behavior change without modifying the loop itself.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

delegate string StringTransformer(string input);

class Program
{
    static string ToUpperCase(string input) => input.ToUpper();

    static void ProcessNames(List<string> names, StringTransformer transformer)
    {
        foreach (string name in names)
        {
            Console.WriteLine(transformer(name));
        }
    }

    static void Main()
    {
        List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
        ProcessNames(names, ToUpperCase);
    }
}Code language: C# (cs)

Explanation:

  • delegate string StringTransformer(string input): Defines a delegate type for any method that takes a string and returns a transformed string.
  • ProcessNames(List<string> names, StringTransformer transformer): Accepts the transformation logic as a parameter instead of hardcoding it inside the method.
  • transformer(name): Invokes whichever method was passed in, applying it to each name during the loop.
  • Flexibility: Passing ToLowerCase or a reverse method instead of ToUpperCase changes the output without touching ProcessNames at all.

Exercise 3: Filter List of Integers

Practice Problem: Create a custom delegate IntFilter that takes an integer and returns a boolean. Write a method FilterList(List<int> numbers, IntFilter filter) that returns a new list containing only the elements that satisfy the condition.

Purpose: This exercise helps you practice using a delegate as a condition that gets evaluated at runtime, which is essentially how methods like List<T>.Where and List<T>.FindAll work internally.

Given Input: numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], filtered using an IsEven condition.

Expected Output: Filtered = 2, 4, 6, 8, 10

▼ Hint
  • Declare the delegate as bool IntFilter(int number).
  • Write an IsEven method that returns true when a number is divisible by 2.
  • Inside FilterList, loop through the input list and call the delegate on each element to decide whether to keep it.
  • Add matching elements to a new list and return that list at the end.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

delegate bool IntFilter(int number);

class Program
{
    static bool IsEven(int number) => number % 2 == 0;

    static List<int> FilterList(List<int> numbers, IntFilter filter)
    {
        List<int> result = new List<int>();
        foreach (int number in numbers)
        {
            if (filter(number))
            {
                result.Add(number);
            }
        }
        return result;
    }

    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        List<int> evens = FilterList(numbers, IsEven);

        Console.WriteLine("Filtered = " + string.Join(", ", evens));
    }
}Code language: C# (cs)

Explanation:

  • delegate bool IntFilter(int number): Defines a delegate representing any condition that takes an integer and returns true or false.
  • filter(number): Calls whichever condition method was passed in, so the filtering rule is decided by the caller, not by FilterList itself.
  • result.Add(number): Builds a new list containing only the numbers that satisfied the condition, leaving the original list unchanged.
  • Reusability: Passing a different method, such as one checking for positive numbers, reuses FilterList without any changes to its code.

Exercise 4: Multicast Logger

Practice Problem: Create a LogMessage delegate. Write three methods that log a message to the console, a mock text file, and a mock debug window. Combine all three into a single multicast delegate and invoke it once.

Purpose: This exercise helps you practice combining multiple methods into one multicast delegate using the += operator, which is the exact mechanism that powers events in C#.

Given Input: message = "System started successfully"

Expected Output:

Console: System started successfully
File: System started successfully
Debug: System started successfully
▼ Hint
  • Declare the delegate as void LogMessage(string message).
  • Write three methods with that same signature, one per logging target.
  • Combine the methods into one delegate variable using the += operator.
  • Invoke the combined delegate a single time and observe all three methods running in the order they were added.
▼ Solution & Explanation
using System;

delegate void LogMessage(string message);

class Program
{
    static void LogToConsole(string message) => Console.WriteLine("Console: " + message);
    static void LogToFile(string message) => Console.WriteLine("File: " + message);
    static void LogToDebug(string message) => Console.WriteLine("Debug: " + message);

    static void Main()
    {
        LogMessage logger = LogToConsole;
        logger += LogToFile;
        logger += LogToDebug;

        logger("System started successfully");
    }
}Code language: C# (cs)

Explanation:

  • LogMessage logger = LogToConsole: Starts the multicast delegate with a single method.
  • logger += LogToFile: Adds another method to the invocation list without removing the first one.
  • logger += LogToDebug: Adds a third method, so the delegate now holds three methods in its invocation list.
  • logger("System started successfully"): Runs every method in the invocation list, in order, with a single call.

Exercise 5: Anonymous Greeting

Practice Problem: Rewrite a simple greeting delegate using an anonymous method with the delegate keyword instead of a separate named method.

Purpose: This exercise helps you practice writing anonymous methods, a step toward lambda expressions, and understand how they simplify short pieces of delegate logic that don’t need to be reused elsewhere.

Given Input: name = "Priya"

Expected Output: Hello, Priya! Welcome.

▼ Hint

Use the delegate keyword directly when assigning the delegate variable, and write the greeting logic inline instead of creating a separate named method.

▼ Solution & Explanation
using System;

delegate void Greet(string name);

class Program
{
    static void Main()
    {
        Greet greet = delegate (string name)
        {
            Console.WriteLine("Hello, " + name + "! Welcome.");
        };

        greet("Priya");
    }
}Code language: C# (cs)

Explanation:

  • delegate (string name) { ... }: Defines the method body inline, right where the delegate is assigned, instead of pointing to a separately declared method.
  • No method name: The block has no name of its own, which is why it is called an anonymous method.
  • greet("Priya"): Invokes the inline logic exactly the same way you would invoke a delegate pointing to a named method.

Exercise 6: Lambda Conversion

Practice Problem: Take the anonymous method from Exercise 5 and rewrite it using a modern lambda expression (=>).

Purpose: This exercise helps you practice lambda syntax as a shorter alternative to anonymous methods, a style you will see constantly in LINQ queries and event handler assignments.

Given Input: name = "Priya"

Expected Output: Hello, Priya! Welcome.

▼ Hint

Replace the delegate (parameters) { } syntax with (parameters) => expression, keeping the same logic inside.

▼ Solution & Explanation
using System;

delegate void Greet(string name);

class Program
{
    static void Main()
    {
        Greet greet = name => Console.WriteLine("Hello, " + name + "! Welcome.");

        greet("Priya");
    }
}Code language: C# (cs)

Explanation:

  • name => Console.WriteLine(...): The part before => lists the parameter, and the part after it is the logic that runs, with no delegate keyword or curly braces needed for a single statement.
  • Type inference: The compiler infers that name is a string because the Greet delegate signature requires it.
  • Same behavior: The lambda version behaves identically to the anonymous method version, just with less code.

Exercise 7: Action Reporter

Practice Problem: Use the built-in Action<string, ConsoleColor> delegate to build a console reporting system that prints messages in different colors, without declaring a custom delegate type.

Purpose: This exercise helps you practice using the built-in Action<> family of delegates, which removes the need to declare a custom void-returning delegate for common cases.

Given Input: Three messages with associated colors: ("Info: Load complete", Green), ("Warning: Low disk space", Yellow), ("Error: Connection failed", Red).

Expected Output:

Info: Load complete
Warning: Low disk space
Error: Connection failed
▼ Hint
  • Declare a variable of type Action<string, ConsoleColor> pointing to a method that matches those two parameters.
  • Inside that method, set Console.ForegroundColor before printing the message.
  • Reset the color back to default after printing, so it doesn’t affect later output.
  • Call the action multiple times with different message and color combinations.
▼ Solution & Explanation
using System;

class Program
{
    static void Report(string message, ConsoleColor color)
    {
        Console.ForegroundColor = color;
        Console.WriteLine(message);
        Console.ResetColor();
    }

    static void Main()
    {
        Action<string, ConsoleColor> report = Report;

        report("Info: Load complete", ConsoleColor.Green);
        report("Warning: Low disk space", ConsoleColor.Yellow);
        report("Error: Connection failed", ConsoleColor.Red);
    }
}Code language: C# (cs)

Explanation:

  • Action<string, ConsoleColor>: A built-in delegate type that represents any method taking a string and a ConsoleColor and returning nothing, so no custom delegate declaration is needed.
  • Console.ForegroundColor = color: Changes the text color for whatever is printed next.
  • Console.ResetColor(): Restores the default console color so later output isn’t affected by a previous call.
  • report(message, color): Invokes the same logic three times with different arguments, printing each message in its own color.

Exercise 8: Func Calculator

Practice Problem: Use the built-in Func<double, double, double> delegate to implement a basic calculator capable of division, checking for division by zero before the calculation runs.

Purpose: This exercise helps you practice using the built-in Func<> family of delegates for methods that return a value, a pattern used throughout LINQ and functional-style C# code.

Given Input: a = 10, b = 2 and then a = 10, b = 0 to test the zero check.

Expected Output:

Result = 5
Cannot divide by zero
▼ Hint
  • Declare a variable of type Func<double, double, double> pointing to a Divide method.
  • Before invoking the function, check whether the divisor is zero.
  • If it is zero, print an error message instead of calling the function.
  • Otherwise, invoke the function and print the returned result.
▼ Solution & Explanation
using System;

class Program
{
    static double Divide(double a, double b) => a / b;

    static void Calculate(Func<double, double, double> operation, double a, double b)
    {
        if (b == 0)
        {
            Console.WriteLine("Cannot divide by zero");
            return;
        }

        Console.WriteLine("Result = " + operation(a, b));
    }

    static void Main()
    {
        Func<double, double, double> divide = Divide;

        Calculate(divide, 10, 2);
        Calculate(divide, 10, 0);
    }
}Code language: C# (cs)

Explanation:

  • Func<double, double, double>: A built-in delegate type representing a method with two double parameters that returns a double, so Divide fits it directly.
  • if (b == 0): Guards against invoking the delegate with an invalid divisor, avoiding a runtime exception.
  • operation(a, b): Invokes whichever function was passed into Calculate, keeping the division logic separate from the safety check.
  • Reusability: Passing a different Func<double, double, double>, such as one for multiplication, would work with Calculate without any changes.

Exercise 9: Predicate Matcher

Practice Problem: Use Predicate<T> to find the first book in a List<Book> that matches a specific criteria, such as being published before a certain year.

Purpose: This exercise helps you practice using Predicate<T> together with List<T>.Find, a built-in search mechanism that avoids writing a manual loop.

Given Input: A list of books, searching for the first one published before 2000.

Expected Output: Found: The Great Gatsby (1925)

▼ Hint
  • Define a simple Book class with Title and Year properties.
  • Create a List<Book> containing a few sample books with different years.
  • Use List<Book>.Find with a Predicate<Book> that checks whether Year is less than 2000.
  • Print the matched book’s title and year, or a not-found message if Find returns null.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

class Book
{
    public string Title { get; set; }
    public int Year { get; set; }
}

class Program
{
    static void Main()
    {
        List<Book> books = new List<Book>
        {
            new Book { Title = "The Great Gatsby", Year = 1925 },
            new Book { Title = "Clean Code", Year = 2008 },
            new Book { Title = "1984", Year = 1949 }
        };

        Predicate<Book> publishedBefore2000 = book => book.Year < 2000;

        Book match = books.Find(publishedBefore2000);

        if (match != null)
        {
            Console.WriteLine("Found: " + match.Title + " (" + match.Year + ")");
        }
        else
        {
            Console.WriteLine("No matching book found");
        }
    }
}Code language: C# (cs)

Explanation:

  • Predicate<Book>: A built-in delegate type representing a method that takes a Book and returns a bool, used to describe a matching condition.
  • book => book.Year < 2000: A lambda expression supplying the actual condition without writing a separate named method.
  • books.Find(publishedBefore2000): Scans the list internally and returns the first book that satisfies the predicate, or null if none match.
  • Null check: Checking match != null avoids a runtime error when no book satisfies the condition.

Exercise 10: The Button Click

Practice Problem: Create a Button class. Simulate a user clicking the button by defining a custom delegate and an OnClick event, then raise the event when a Click() method is called.

Purpose: This exercise helps you practice the relationship between delegates and events. An event is a controlled way of exposing a delegate so outside code can subscribe to it, without being able to invoke it directly or overwrite existing subscribers.

Given Input: A handler method is subscribed to OnClick before button.Click() is called.

Expected Output: Button was clicked!

▼ Hint
  • Declare a delegate such as void ClickHandler().
  • Inside the Button class, declare an event of that delegate type, for example event ClickHandler OnClick.
  • Write a Click() method that checks whether OnClick has any subscribers before invoking it.
  • Outside the class, subscribe a handler method to OnClick using += before calling Click().
▼ Solution & Explanation
using System;

delegate void ClickHandler();

class Button
{
    public event ClickHandler OnClick;

    public void Click()
    {
        OnClick?.Invoke();
    }
}

class Program
{
    static void ShowClickMessage()
    {
        Console.WriteLine("Button was clicked!");
    }

    static void Main()
    {
        Button button = new Button();
        button.OnClick += ShowClickMessage;

        button.Click();
    }
}Code language: C# (cs)

Explanation:

  • event ClickHandler OnClick: Exposes the delegate as an event, so outside code can only subscribe or unsubscribe with += and -=, not invoke it or replace it directly.
  • OnClick?.Invoke(): Safely raises the event only if at least one method has subscribed, avoiding a null reference error when there are no subscribers.
  • button.OnClick += ShowClickMessage: Subscribes an external method to the event from outside the Button class.
  • button.Click(): Triggers the internal logic that raises the event, running every subscribed method in response.

Exercise 11: Stock Price Tracker

Practice Problem: Create a Stock class with a Price property. Raise a PriceChanged event whenever the price changes, notifying a StockTrader class that prints the old and new prices.

Purpose: This exercise helps you practice raising an event from inside a property setter whenever state changes, and letting an external class react to that change, the core mechanism behind property change notifications.

Given Input: Price starts at 100, then changes to 150, then to 165.

Expected Output:

Price changed from 100 to 150
Price changed from 150 to 165
▼ Hint
  • Store the price in a private backing field and only raise the event from inside the Price property’s setter.
  • Compare the incoming value against the current price before assigning it, so the event only fires on an actual change.
  • Declare a delegate that carries both the old price and the new price as parameters.
  • Subscribe the StockTrader method to the event before changing the price so it receives the notification.
▼ Solution & Explanation
using System;

delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice);

class Stock
{
    private decimal price;

    public event PriceChangedHandler PriceChanged;

    public decimal Price
    {
        get => price;
        set
        {
            if (value != price)
            {
                decimal oldPrice = price;
                price = value;
                PriceChanged?.Invoke(oldPrice, price);
            }
        }
    }
}

class StockTrader
{
    public void OnPriceChanged(decimal oldPrice, decimal newPrice)
    {
        Console.WriteLine("Price changed from " + oldPrice + " to " + newPrice);
    }
}

class Program
{
    static void Main()
    {
        Stock stock = new Stock { Price = 100 };
        StockTrader trader = new StockTrader();

        stock.PriceChanged += trader.OnPriceChanged;

        stock.Price = 150;
        stock.Price = 165;
    }
}Code language: C# (cs)

Explanation:

  • private decimal price: A backing field that stores the actual value separately from the public property, so the old value is still available when the event fires.
  • if (value != price): Ensures the event only fires when the new price is genuinely different, avoiding redundant notifications.
  • PriceChanged?.Invoke(oldPrice, price): Raises the event safely and passes both the old and new values to every subscriber.
  • stock.PriceChanged += trader.OnPriceChanged: Subscribes an instance method from a completely separate class, showing that events can connect unrelated classes together.

Exercise 12: Thermostat Alert

Practice Problem: Design a Thermostat class that fires a TemperatureCritical event if the temperature rises above 100°C.

Purpose: This exercise helps you practice conditionally raising an event only when a threshold is crossed, a pattern common in monitoring, alerting, and safety-critical systems.

Given Input: Temperature is set to 85, then to 105.

Expected Output: Critical temperature reached: 105°C

▼ Hint
  • Store the temperature using a property with a backing field, similar to the Stock Price Tracker exercise.
  • Inside the setter, check whether the new value is greater than 100 after assigning it.
  • Only invoke TemperatureCritical when that condition is true, passing the current temperature.
  • Subscribe a handler before setting the temperature so you can observe when the alert does and doesn’t fire.
▼ Solution & Explanation
using System;

delegate void TemperatureCriticalHandler(double temperature);

class Thermostat
{
    private double temperature;

    public event TemperatureCriticalHandler TemperatureCritical;

    public double Temperature
    {
        get => temperature;
        set
        {
            temperature = value;
            if (temperature > 100)
            {
                TemperatureCritical?.Invoke(temperature);
            }
        }
    }
}

class Program
{
    static void Main()
    {
        Thermostat thermostat = new Thermostat();
        thermostat.TemperatureCritical += temp =>
            Console.WriteLine("Critical temperature reached: " + temp + "°C");

        thermostat.Temperature = 85;
        thermostat.Temperature = 105;
    }
}Code language: C# (cs)

Explanation:

  • if (temperature > 100): Guards the event so it only fires once the reading crosses the critical threshold.
  • TemperatureCritical?.Invoke(temperature): Passes the current reading to every subscriber at the moment the alert condition is met.
  • Lambda subscriber: Subscribes an inline lambda instead of a separate named method, printing the alert message directly.
  • Setting Temperature = 85 first: Shows that the event does not fire for a normal reading, only for one that trips the threshold.

Exercise 13: Unsubscribing Memory Leak

Practice Problem: Write a program where an object subscribes to an event, fires it, unsubscribes, and fires it again. Verify that the second fire does not trigger the unsubscribed object.

Purpose: This exercise helps you practice using -= to detach a handler from an event, which matters for preventing memory leaks caused by subscribers that outlive the object they are attached to.

Given Input: Subscribe Handler to MyEvent, raise the event, unsubscribe Handler, then raise the event again.

Expected Output: Handler triggered (printed only once, after the first raise)

▼ Hint
  • Declare a simple event with a delegate that takes no parameters.
  • Subscribe a method using += and raise the event to confirm it runs.
  • Unsubscribe using -= with the exact same method reference used to subscribe.
  • Raise the event a second time and confirm nothing prints, since the invocation list is now empty.
▼ Solution & Explanation
using System;

delegate void Notify();

class Publisher
{
    public event Notify MyEvent;

    public void RaiseEvent()
    {
        MyEvent?.Invoke();
    }
}

class Program
{
    static void Handler()
    {
        Console.WriteLine("Handler triggered");
    }

    static void Main()
    {
        Publisher publisher = new Publisher();

        publisher.MyEvent += Handler;
        publisher.RaiseEvent();

        publisher.MyEvent -= Handler;
        publisher.RaiseEvent();
    }
}Code language: C# (cs)

Explanation:

  • publisher.MyEvent += Handler: Adds Handler to the invocation list so it runs the next time the event is raised.
  • First RaiseEvent(): Prints “Handler triggered” because Handler is still subscribed at that point.
  • publisher.MyEvent -= Handler: Removes Handler from the invocation list using the same method reference that was used to subscribe.
  • Second RaiseEvent(): Produces no output, confirming the unsubscribe worked and the object is no longer referenced by the event’s invocation list.

Exercise 14: Standard .NET Event Pattern

Practice Problem: Redo the Stock Price Tracker (Exercise 11) using the standard .NET guidelines: use EventHandler<T> and a custom EventArgs subclass named StockChangedEventArgs.

Purpose: This exercise helps you practice the standard .NET event pattern that every framework and library event follows: a sender object, a piece of data derived from EventArgs, and the generic EventHandler<T> delegate.

Given Input: Price starts at 100, then changes to 150, then to 165.

Expected Output:

Price changed from 100 to 150
Price changed from 150 to 165
▼ Hint
  • Create a StockChangedEventArgs class that inherits from EventArgs and exposes OldPrice and NewPrice properties.
  • Declare the event using EventHandler<StockChangedEventArgs> instead of a custom delegate type.
  • Make sure the handler method signature matches (object sender, StockChangedEventArgs e).
  • Raise the event by passing this as the sender and a new StockChangedEventArgs instance as the data.
▼ Solution & Explanation
using System;

class StockChangedEventArgs : EventArgs
{
    public decimal OldPrice { get; }
    public decimal NewPrice { get; }

    public StockChangedEventArgs(decimal oldPrice, decimal newPrice)
    {
        OldPrice = oldPrice;
        NewPrice = newPrice;
    }
}

class Stock
{
    private decimal price;

    public event EventHandler<StockChangedEventArgs> PriceChanged;

    public decimal Price
    {
        get => price;
        set
        {
            if (value != price)
            {
                decimal oldPrice = price;
                price = value;
                PriceChanged?.Invoke(this, new StockChangedEventArgs(oldPrice, price));
            }
        }
    }
}

class Program
{
    static void OnPriceChanged(object sender, StockChangedEventArgs e)
    {
        Console.WriteLine("Price changed from " + e.OldPrice + " to " + e.NewPrice);
    }

    static void Main()
    {
        Stock stock = new Stock { Price = 100 };
        stock.PriceChanged += OnPriceChanged;

        stock.Price = 150;
        stock.Price = 165;
    }
}Code language: C# (cs)

Explanation:

  • StockChangedEventArgs : EventArgs: A custom data container following the .NET convention of deriving from EventArgs to bundle event data together.
  • EventHandler<StockChangedEventArgs> PriceChanged: Uses the standard generic delegate instead of a custom one, matching the pattern used throughout the .NET framework.
  • PriceChanged?.Invoke(this, new StockChangedEventArgs(...)): Passes the publishing object as the sender along with the event data as a single package.
  • OnPriceChanged(object sender, StockChangedEventArgs e): Matches the required handler signature for EventHandler<T>, giving access to both the sender and the event data.

Exercise 15: Safe Event Invocation

Practice Problem: Implement a class that raises an event safely using the null-conditional operator (MyEvent?.Invoke(this, EventArgs.Empty)) to avoid a null reference exception when there are no subscribers.

Purpose: This exercise helps you practice the standard safe-invocation idiom, and understand why checking for null before invoking an event matters, especially when subscribers could be added or removed on another thread.

Given Input: RaiseEvent() is called once before any subscriber is attached, then a handler subscribes, then RaiseEvent() is called again.

Expected Output: Event raised safely (printed once, only after a subscriber is attached; the first call produces no output and no exception).

▼ Hint
  • Declare the event using the built-in EventHandler type.
  • Inside the method that raises the event, use the null-conditional operator instead of calling Invoke directly.
  • Call the raising method once before subscribing anything, to confirm no exception occurs.
  • Subscribe a handler afterward and call the raising method again to see it print.
▼ Solution & Explanation
using System;

class Publisher
{
    public event EventHandler MyEvent;

    public void RaiseEvent()
    {
        MyEvent?.Invoke(this, EventArgs.Empty);
    }
}

class Program
{
    static void Handler(object sender, EventArgs e)
    {
        Console.WriteLine("Event raised safely");
    }

    static void Main()
    {
        Publisher publisher = new Publisher();

        publisher.RaiseEvent();

        publisher.MyEvent += Handler;
        publisher.RaiseEvent();
    }
}Code language: C# (cs)

Explanation:

  • MyEvent?.Invoke(this, EventArgs.Empty): Checks whether MyEvent is null before invoking it, avoiding a NullReferenceException when nobody has subscribed.
  • First RaiseEvent() call: Produces no output and no crash, because the null-conditional operator skips the invocation entirely.
  • EventArgs.Empty: A reusable, pre-built empty instance of EventArgs, convenient when an event doesn’t carry any extra data.
  • Second RaiseEvent() call: Prints “Event raised safely” because Handler is now subscribed.

Exercise 16: Text File Downloader

Practice Problem: Create a FileDownloader class. Implement a ProgressChanged event using EventHandler<DownloadProgressEventArgs> that passes the percentage completion back to the console UI.

Purpose: This exercise helps you practice modeling a progress-reporting workflow with the standard event pattern, similar to how real download or upload APIs report status back to a UI layer.

Given Input: A simulated download that reports progress in steps of 25, up to 100.

Expected Output:

Download progress: 25%
Download progress: 50%
Download progress: 75%
Download progress: 100%
▼ Hint
  • Create a DownloadProgressEventArgs class that inherits from EventArgs and holds a PercentComplete property.
  • Declare event EventHandler<DownloadProgressEventArgs> ProgressChanged inside FileDownloader.
  • Write a StartDownload method that loops through progress increments, raising the event at each step.
  • Subscribe a console-printing handler before calling StartDownload.
▼ Solution & Explanation
using System;

class DownloadProgressEventArgs : EventArgs
{
    public int PercentComplete { get; }

    public DownloadProgressEventArgs(int percentComplete)
    {
        PercentComplete = percentComplete;
    }
}

class FileDownloader
{
    public event EventHandler<DownloadProgressEventArgs> ProgressChanged;

    public void StartDownload()
    {
        for (int percent = 25; percent <= 100; percent += 25)
        {
            ProgressChanged?.Invoke(this, new DownloadProgressEventArgs(percent));
        }
    }
}

class Program
{
    static void OnProgressChanged(object sender, DownloadProgressEventArgs e)
    {
        Console.WriteLine("Download progress: " + e.PercentComplete + "%");
    }

    static void Main()
    {
        FileDownloader downloader = new FileDownloader();
        downloader.ProgressChanged += OnProgressChanged;

        downloader.StartDownload();
    }
}Code language: C# (cs)

Explanation:

  • DownloadProgressEventArgs : EventArgs: Bundles the percentage value into a dedicated data class, following the standard event pattern.
  • for (int percent = 25; percent <= 100; percent += 25): Simulates the download advancing in fixed increments, raising the event at each step.
  • ProgressChanged?.Invoke(this, new DownloadProgressEventArgs(percent)): Reports progress to any subscriber without FileDownloader needing to know how it will be displayed.
  • OnProgressChanged: Reads e.PercentComplete from the event data and prints it, keeping console-specific formatting outside of FileDownloader.

Exercise 17: Generic Data Pipeline

Practice Problem: Write a generic class Pipeline<T> that accepts data and passes it through a chain of generic Func<T, T> delegates to transform the data step by step.

Purpose: This exercise helps you practice combining generics with delegates to build a flexible, reusable processing pipeline, a pattern common in data transformation and middleware-style code.

Given Input: Starting value 3, passed through three steps: double it, add 5, then square it.

Expected Output: Result = 121

▼ Hint
  • Give Pipeline<T> an internal list of Func<T, T> steps.
  • Write an AddStep method that appends a transformation function to that list.
  • Write a Process(T input) method that loops through the steps, feeding the output of one step as the input to the next.
  • Add the steps in the exact order they should run, then call Process with the starting value.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

class Pipeline<T>
{
    private List<Func<T, T>> steps = new List<Func<T, T>>();

    public void AddStep(Func<T, T> step)
    {
        steps.Add(step);
    }

    public T Process(T input)
    {
        T result = input;
        foreach (Func<T, T> step in steps)
        {
            result = step(result);
        }
        return result;
    }
}

class Program
{
    static void Main()
    {
        Pipeline<int> pipeline = new Pipeline<int>();

        pipeline.AddStep(x => x * 2);
        pipeline.AddStep(x => x + 5);
        pipeline.AddStep(x => x * x);

        int result = pipeline.Process(3);
        Console.WriteLine("Result = " + result);
    }
}Code language: C# (cs)

Explanation:

  • Pipeline<T>: A generic class that can transform any data type, not just integers, as long as matching Func<T, T> steps are supplied.
  • AddStep(Func<T, T> step): Appends a transformation to an internal list without running it immediately.
  • foreach (Func<T, T> step in steps): Applies each stored transformation in sequence, feeding the result of one step into the next.
  • pipeline.Process(3): Runs the value 3 through all three steps in order: 3 → 6 → 11 → 121.

Exercise 18: BankAccount Overdraft Protection

Practice Problem: Create a BankAccount class with a Withdraw method. If a withdrawal exceeds the balance, fire an OverdraftAttempted event, and allow the subscriber to cancel the transaction using a property inside the event data.

Purpose: This exercise helps you practice using event data to let a subscriber influence the outcome of an operation, a pattern used for cancellable events such as closing a window or validating a form before it submits.

Given Input: Balance starts at 100. A withdrawal of 150 is attempted, and the subscriber sets Cancel = true.

Expected Output:

Overdraft attempted for 150
Transaction cancelled by subscriber
Balance remains: 100
▼ Hint
  • Create an OverdraftEventArgs class inheriting from EventArgs with a settable Cancel property, defaulting to false.
  • Inside Withdraw, check if the requested amount exceeds the balance before raising OverdraftAttempted.
  • After raising the event, check the Cancel property on that same event data instance to decide what happens next.
  • If Cancel is true, print a cancellation message and leave the balance unchanged; otherwise complete the withdrawal as usual.
▼ Solution & Explanation
using System;

class OverdraftEventArgs : EventArgs
{
    public decimal AttemptedAmount { get; }
    public bool Cancel { get; set; }

    public OverdraftEventArgs(decimal attemptedAmount)
    {
        AttemptedAmount = attemptedAmount;
        Cancel = false;
    }
}

class BankAccount
{
    public decimal Balance { get; private set; }

    public event EventHandler<OverdraftEventArgs> OverdraftAttempted;

    public BankAccount(decimal initialBalance)
    {
        Balance = initialBalance;
    }

    public void Withdraw(decimal amount)
    {
        if (amount > Balance)
        {
            OverdraftEventArgs args = new OverdraftEventArgs(amount);
            OverdraftAttempted?.Invoke(this, args);

            if (args.Cancel)
            {
                Console.WriteLine("Transaction cancelled by subscriber");
                Console.WriteLine("Balance remains: " + Balance);
                return;
            }
        }

        Balance -= amount;
        Console.WriteLine("Withdrawal successful. Balance: " + Balance);
    }
}

class Program
{
    static void Main()
    {
        BankAccount account = new BankAccount(100);

        account.OverdraftAttempted += (sender, e) =>
        {
            Console.WriteLine("Overdraft attempted for " + e.AttemptedAmount);
            e.Cancel = true;
        };

        account.Withdraw(150);
    }
}Code language: C# (cs)

Explanation:

  • OverdraftEventArgs with a Cancel property: Carries both the attempted amount and a writable flag that a subscriber can set to influence what happens next.
  • OverdraftAttempted?.Invoke(this, args): Raises the event and passes the same args instance to every subscriber, so any changes they make to Cancel stay visible afterward.
  • if (args.Cancel): Checked immediately after raising the event, letting the subscriber’s decision control whether the withdrawal proceeds.
  • e.Cancel = true inside the subscriber: Demonstrates how external code can cancel an operation started inside another class, without BankAccount needing to know why.

Exercise 19: The Chat Room Broadcast

Practice Problem: Build a ChatRoom class where multiple User objects can join. When one user sends a message, the ChatRoom broadcasts it to all other connected users through events.

Purpose: This exercise helps you practice using events for one-to-many communication between independent objects, similar to how publish-subscribe messaging systems work.

Given Input: Three users, Alice, Bob, and Charlie, join the room. Alice sends "Hello everyone!".

Expected Output:

Bob received: Hello everyone!
Charlie received: Hello everyone!
▼ Hint
  • Give User its own MessageSent event that fires whenever that user sends a message.
  • When a user joins the room, have ChatRoom subscribe to that user’s MessageSent event.
  • When ChatRoom receives a message from a sender, loop through every joined user and deliver the message to each one.
  • Skip the original sender in that loop, so a user never receives their own broadcasted message.
▼ Solution & Explanation
using System;
using System.Collections.Generic;

class User
{
    public string Name { get; }
    public event Action<User, string> MessageSent;

    public User(string name)
    {
        Name = name;
    }

    public void Send(string message)
    {
        MessageSent?.Invoke(this, message);
    }

    public void Receive(string message)
    {
        Console.WriteLine(Name + " received: " + message);
    }
}

class ChatRoom
{
    private List<User> users = new List<User>();

    public void Join(User user)
    {
        users.Add(user);
        user.MessageSent += Broadcast;
    }

    private void Broadcast(User sender, string message)
    {
        foreach (User user in users)
        {
            if (user != sender)
            {
                user.Receive(message);
            }
        }
    }
}

class Program
{
    static void Main()
    {
        ChatRoom room = new ChatRoom();

        User alice = new User("Alice");
        User bob = new User("Bob");
        User charlie = new User("Charlie");

        room.Join(alice);
        room.Join(bob);
        room.Join(charlie);

        alice.Send("Hello everyone!");
    }
}Code language: C# (cs)

Explanation:

  • event Action<User, string> MessageSent on User: Lets ChatRoom know whenever a user sends a message, without User needing a direct reference to ChatRoom.
  • room.Join(user): Registers the user in the room’s list and subscribes to that user’s MessageSent event in a single step.
  • Broadcast(User sender, string message): Loops through every joined user and delivers the message, skipping the original sender.
  • alice.Send(...): Triggers the whole chain, from Alice raising her own event to ChatRoom delivering the message to Bob and Charlie.

Exercise 20: Traffic Light System

Practice Problem: Create a TrafficLightController that manages Red, Yellow, and Green state transitions. Use events to notify attached Car and Pedestrian objects so each can change its own behavior.

Purpose: This exercise helps you practice broadcasting a single state change to multiple different subscriber types at once, showing that an event isn’t limited to notifying just one kind of listener.

Given Input: The controller changes from its starting state to Green, then Yellow, then Red.

Expected Output:

Car: Driving through green light
Pedestrian: Do not cross
Car: Slowing down for yellow light
Pedestrian: Do not cross
Car: Stopped at red light
Pedestrian: Waiting to cross
▼ Hint
  • Define an enum with values for Red, Yellow, and Green.
  • Declare a single event on TrafficLightController that carries the new state as its parameter.
  • Write a method that updates the state and raises the event, and call it once per transition.
  • Have both Car and Pedestrian subscribe their own methods to the same event, each reacting differently to the same state.
▼ Solution & Explanation
using System;

enum LightState
{
    Red,
    Yellow,
    Green
}

class TrafficLightController
{
    public event Action<LightState> LightChanged;

    public void ChangeLight(LightState newState)
    {
        LightChanged?.Invoke(newState);
    }
}

class Car
{
    public void OnLightChanged(LightState state)
    {
        switch (state)
        {
            case LightState.Green:
                Console.WriteLine("Car: Driving through green light");
                break;
            case LightState.Yellow:
                Console.WriteLine("Car: Slowing down for yellow light");
                break;
            case LightState.Red:
                Console.WriteLine("Car: Stopped at red light");
                break;
        }
    }
}

class Pedestrian
{
    public void OnLightChanged(LightState state)
    {
        Console.WriteLine(state == LightState.Red ? "Pedestrian: Waiting to cross" : "Pedestrian: Do not cross");
    }
}

class Program
{
    static void Main()
    {
        TrafficLightController controller = new TrafficLightController();
        Car car = new Car();
        Pedestrian pedestrian = new Pedestrian();

        controller.LightChanged += car.OnLightChanged;
        controller.LightChanged += pedestrian.OnLightChanged;

        controller.ChangeLight(LightState.Green);
        controller.ChangeLight(LightState.Yellow);
        controller.ChangeLight(LightState.Red);
    }
}Code language: C# (cs)

Explanation:

  • event Action<LightState> LightChanged: A single event that any number of different object types can subscribe to, regardless of what each one does in response.
  • controller.LightChanged += car.OnLightChanged and += pedestrian.OnLightChanged: Attaches two unrelated classes to the same event without either one knowing about the other.
  • ChangeLight(newState): The only place that raises the event, keeping the decision of “what happens next” entirely outside TrafficLightController.
  • switch statement inside Car.OnLightChanged: Shows each subscriber deciding its own reaction to the exact same broadcasted state.

Exercise 21: Custom Event Accessors (add / remove)

Practice Problem: Implement an event explicitly using add and remove accessors to log to the console every time a subscriber attaches or detaches from the event.

Purpose: This exercise helps you practice writing custom add and remove accessors, revealing what the compiler normally generates automatically behind a plain event declaration.

Given Input: A handler subscribes to MyEvent, then unsubscribes from it.

Expected Output:

Subscriber added
Subscriber removed
▼ Hint
  • Declare a private backing field of the delegate type to store the actual invocation list yourself.
  • Write the event using explicit add and remove blocks instead of the shorthand event declaration.
  • Inside add, append the incoming handler to the backing field and print a log message.
  • Inside remove, detach the incoming handler from the backing field and print a log message.
▼ Solution & Explanation
using System;

delegate void Notify();

class Publisher
{
    private Notify handlers;

    public event Notify MyEvent
    {
        add
        {
            handlers += value;
            Console.WriteLine("Subscriber added");
        }
        remove
        {
            handlers -= value;
            Console.WriteLine("Subscriber removed");
        }
    }

    public void RaiseEvent()
    {
        handlers?.Invoke();
    }
}

class Program
{
    static void Handler()
    {
        Console.WriteLine("Handler triggered");
    }

    static void Main()
    {
        Publisher publisher = new Publisher();

        publisher.MyEvent += Handler;
        publisher.MyEvent -= Handler;
    }
}Code language: C# (cs)

Explanation:

  • private Notify handlers: A manually declared backing field that stores subscribers, since explicit accessors bypass the compiler-generated one.
  • add { handlers += value; ... }: Runs every time someone subscribes with +=, letting you insert custom logic such as logging alongside the actual subscription.
  • remove { handlers -= value; ... }: Runs every time someone unsubscribes with -=, mirroring the add accessor.
  • value: A keyword representing the method being subscribed or unsubscribed, available automatically inside both accessors.

Filed Under: C# Exercises

Did you find this page helpful? Let others know about it. Sharing helps me continue to create free Python resources.

TweetF  sharein  shareP  Pin

About Vishal

I’m Vishal Hule, the Founder of PYnative.com. As a Python developer, I enjoy assisting students, developers, and learners. Follow me on Twitter.

Related Tutorial Topics:

C# Exercises

All Coding Exercises:

C Exercises
C++ Exercises
Python Exercises
Java Exercises
C# Exercises

Python Exercises and Quizzes

Free coding exercises and quizzes cover Python basics, data structure, data analytics, and more.

  • 15+ Topic-specific Exercises and Quizzes
  • Each Exercise contains 25+ questions
  • Each Quiz contains 25 MCQ
Exercises
Quizzes

Leave a Reply Cancel reply

your email address will NOT be published. all comments are moderated according to our comment policy.

Use <pre> tag for posting code. E.g. <pre> Your entire code </pre>

In: C# Exercises
TweetF  sharein  shareP  Pin

  C# Exercises

  • C# Exercise for Beginners
  • C# Loops Exercise
  • C# Array Exercise
  • C# String Exercise
  • C# OOP Exercise
  • C# Structs, Records and Enums Exercise
  • C# Collections Exercise
  • C# List Exercise
  • C# Dictionary Exercise
  • C# LINQ Exercise
  • C# Exception Handling Exercise
  • C# File Handling Exercise
  • C# Date and Time Exercise
  • C# Generics Exercise
  • C# Lambda Expressions Exercise
  • C# Delegates and Events Exercise
  • C# Extension Methods Exercise
  • C# Regex Exercise
  • C# Pattern Matching Exercise
  • C# Iterators and Yield Exercise
  • C# Indexers and Operator Overloading Exercise
  • C# Random Data Generation Exercise

All Coding Exercises

Python Exercises C Exercises C++ Exercises Java Exercises C# Exercises

About PYnative

PYnative.com is for Python lovers. Here, You can get Tutorials, Exercises, and Quizzes to practice and improve your Python skills.

Follow Us

To get New Python Tutorials, Exercises, and Quizzes

  • Twitter
  • Facebook
  • Sitemap

Explore Python

  • Learn Python
  • Python Basics
  • Python Databases
  • Python Exercises
  • Python Quizzes
  • Online Python Code Editor
  • Python Tricks

Coding Exercises

  • C Exercises
  • C++ Exercises
  • Python Exercises
  • Java Exercises
  • C# Exercises

Legal Stuff

  • About Us
  • Contact Us

We use cookies to improve your experience. While using PYnative, you agree to have read and accepted our:

  • Terms Of Use
  • Privacy Policy
  • Cookie Policy

Copyright © 2018–2026 pynative.com