Skip to content

Intermediate · Exceptions, delegates, events and async · topic 28 of 43 · 25 min

Events: letting objects notify each other

Explanation

An event lets one object (the publisher) announce that something happened, while any number of other objects (subscribers) react, without the publisher knowing who they are. You declare it with the event keyword, usually as EventHandler<T>, subscribers attach with += and detach with -=, and the publisher raises it with ?.Invoke(this, args).

Why events?

When a customer places an order, many things should happen: send an SMS, send an email, update stock, award loyalty points. If the Order class called all of these directly, it would depend on every other part of the system, and adding a new reaction would mean editing Order. With an event, Order just announces "order placed", and each part subscribes on its own.

Think of it like this: An event is like a WhatsApp broadcast group for a housing society. The secretary posts "Water supply off tomorrow" once. Every member who joined the group gets the message and reacts in their own way: fill buckets, plan laundry. The secretary does not phone each family, and new families can join or leave any time.

Declaring and raising an event

csharp
public class OrderPlacedEventArgs : EventArgs
{
    public int OrderId { get; }
    public decimal Amount { get; }
    public OrderPlacedEventArgs(int orderId, decimal amount) { OrderId = orderId; Amount = amount; }
}

public class OrderService
{
    public event EventHandler<OrderPlacedEventArgs>? OrderPlaced;

    public void Place(int orderId, decimal amount)
    {
        // ... save the order ...
        OnOrderPlaced(new OrderPlacedEventArgs(orderId, amount));
    }

    protected virtual void OnOrderPlaced(OrderPlacedEventArgs e) => OrderPlaced?.Invoke(this, e);
}
EventHandler<T> is the standard delegate for events: (object? sender, T e). sender is who raised it; e carries the details. The ? on the event means it is null when no one has subscribed.

Subscribing and unsubscribing

csharp
var service = new OrderService();

service.OrderPlaced += (sender, e) => Console.WriteLine($"SMS: order {e.OrderId} confirmed");
service.OrderPlaced += SendEmail;

service.Place(1001, 799m);

service.OrderPlaced -= SendEmail;     // stop listening
service.Place(1002, 250m);

static void SendEmail(object? sender, OrderPlacedEventArgs e)
    => Console.WriteLine($"Email: receipt for Rs {e.Amount}");
The first order triggers SMS and Email. The second triggers only the SMS, because SendEmail was removed.

What the event keyword adds

An event is a delegate with a protective wrapper. Outside code can only add (+=) and remove (-=) handlers. It cannot raise the event, and it cannot wipe out everyone else's handlers with =. Only the class that declares the event can raise it.

csharp
// service.OrderPlaced = null;                  // error: only += and -= allowed outside
// service.OrderPlaced?.Invoke(null, args);     // error: cannot raise from outside
With a plain public delegate field, both of these lines would compile and cause bugs.

Where you will meet events

  • Desktop UI: button.Click += OnSaveClicked; in Windows Forms and WPF.
  • Timers: System.Timers.Timer raises Elapsed every interval.
  • File watching: FileSystemWatcher raises Created and Changed when files appear or change.
  • Games: Unity code often uses C# events (or UnityEvents) for "player died" or "coin collected".

Common mistakes and fixes

  • Raising without ?.: OrderPlaced(this, e) throws NullReferenceException when nobody subscribed. Use OrderPlaced?.Invoke(this, e).
  • Never unsubscribing: the publisher keeps a reference to each subscriber, so long-lived publishers can keep short-lived objects alive (a memory leak). Use -= when the subscriber is done.
  • Trying to unsubscribe a lambda by writing the same lambda again: it is a different delegate, so nothing is removed. Store the lambda in a variable, or use a named method.
  • Slow work inside handlers: handlers run one after another on the same thread; a slow one delays the rest.
Tip: Name events in the past tense for things that happened (OrderPlaced, PaymentFailed) and give the raising method an On prefix (OnOrderPlaced).

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Events: letting objects notify each other · 1/7

Events

In this lesson objects will notify each other using events.

Prefer a video by a teacher? Find free videos on “C# events EventHandler publisher subscriber tutorial” on YouTube ↗

In real life: A low-stock alert at a kirana store

A grocery store's inventory class raises a LowStock event when an item falls below its reorder level. The shop owner's phone app and the supplier ordering module both subscribe, and the inventory code never needs to know about either.

csharp
var inv = new Inventory();
inv.LowStock += (_, e) => Console.WriteLine($"Owner alert: only {e.Left} {e.Item} left");
inv.LowStock += (_, e) => Console.WriteLine($"Auto-order: 50 more {e.Item}");

inv.Sell("Atta 5kg", 7);
inv.Sell("Atta 5kg", 2);

public record LowStockEventArgs(string Item, int Left);

public class Inventory
{
    private readonly Dictionary<string, int> _stock = new() { ["Atta 5kg"] = 12 };
    public event EventHandler<LowStockEventArgs>? LowStock;

    public void Sell(string item, int qty)
    {
        _stock[item] -= qty;
        if (_stock[item] < 5)
            LowStock?.Invoke(this, new LowStockEventArgs(item, _stock[item]));
    }
}
The first sale leaves 5, so no alert. The second leaves 3 and both subscribers react. The _ means "I do not need the sender". Records work fine as event arguments.

Practice (0/4 done)

Task 1 · Question

Which operator subscribes a handler to an event?

Task 2 · Question

Why raise an event with Changed?.Invoke(this, e) instead of Changed(this, e)?

Task 3 · Question

What can code OUTSIDE the declaring class do with an event?

Task 4 · Practice

Create a class Thermostat with a Temperature property. When the temperature is set above 40, raise an event HeatAlert with EventHandler<double> carrying the temperature. Subscribe two handlers: one prints a warning, one prints "Turning on cooler". Set the temperature to 35 and then 42.

Quick quiz

Question 1

1.What is the signature of an EventHandler<T> handler?

Question 2

2.What is the main benefit of events?

Question 3

3.Why might a forgotten subscription cause a memory leak?

Question 4

4.What is a good name for an event raised after a payment fails?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is the signature of an EventHandler<T> handler?

Answer: void (object? sender, T e)

Handlers receive the sender and the event data.

What is the main benefit of events?

Answer: The publisher does not need to know who reacts, so parts stay loosely coupled

Events decouple the announcer from the listeners.

Why might a forgotten subscription cause a memory leak?

Answer: The publisher keeps a reference to the subscriber, keeping it alive

As long as the publisher lives and holds the handler, the subscriber cannot be collected.

What is a good name for an event raised after a payment fails?

Answer: PaymentFailed

Events are usually named in the past tense.

What is event delegation?Related

Answer: Putting one listener on a parent and finding the clicked child with event.target

Because events bubble up, one listener on the parent can handle all its children, even ones added later.

From Events: reacting to the user

A worker received an SQS message but crashed before deleting it. What happens?Related

Answer: After the visibility timeout ends, the message becomes visible again

Messages are only removed when deleted. When the visibility timeout ends, another consumer can receive it. It goes to the DLQ only after maxReceiveCount receives.

From Event-driven design: API Gateway, SQS, SNS, EventBridge and Step Functions

In def f(*args), what type is args?Related

Answer: tuple

A single star collects the extra positional arguments into a tuple.

From *args and **kwargs

What does int("abc") raise?Related

Answer: ValueError

The type (a string) is fine for int(), but this value cannot become a number, so it is a ValueError.

From Errors and exceptions

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Exceptions, delegates, events and async” before its exam: async and await: waiting without blocking, Files and JSON with System.Text.Json.
  2. Module 7 exam: Exceptions, delegates, events and async · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Web APIs with ASP.NET Core and EF Core. 43 topics left in the course before the final exam.

← Previous: Delegates, lambdas, Func, Action and Predicate