Object-Oriented C# · Lesson 21

Events and subscription lifetime

Observe state changes and explicitly release a subscription when it is no longer needed.

How it works

An event lets an object notify interested listeners without knowing their concrete types. The publisher controls when the event is raised, while subscribers provide compatible handlers. A small Action-based event is enough to demonstrate the relationship, although a larger public API may use EventHandler<TEventArgs> to provide a more conventional event contract.

Subscriptions have a lifetime. The publisher retains its subscribed delegates, and those delegates can retain listener objects or captured state. When a listener no longer needs updates, unsubscribe using the same delegate identity. This is a relationship between objects, not a background message bus or an automatic thread switch. A normal event handler runs synchronously when the publisher invokes it unless the implementation explicitly does something else.

Walk through the example. The Counter raises Changed after adding a value. The named Handle method increments an observation count and prints the new total. After the first addition, the code unsubscribes that same handler. A second addition changes the counter's total but produces no event output for the former listener. The final values show a total of six and only one observed notification.

Watch the boundary. Subscribing with one anonymous lambda and unsubscribing with a newly written lambda does not reliably identify the original delegate instance. Store the handler when later unsubscription is required, or use a named method with the appropriate target. Also define how handler failures affect the publisher: an exception from a synchronous handler can interrupt the event invocation and the surrounding operation.

Practice independently. Add two listeners, unsubscribe one, and predict which notifications each receives after several updates. Use a stored delegate for one subscription and a named method for the other. Then define whether adding zero should raise an event, and test that policy. Explain why a listener's lifetime should be tied to a clear owner rather than relying on eventual garbage collection to end the subscription.

A working example

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var counter = new Counter();
int observed = 0;
void Handle(int total) { observed++; Console.WriteLine($"changed:{total}"); }
counter.Changed += Handle;
counter.Add(2);
counter.Changed -= Handle;
counter.Add(4);
Console.WriteLine(counter.Total);
Console.WriteLine(observed);
sealed class Counter
{
    public int Total { get; private set; }
    public event Action<int>? Changed;
    public void Add(int amount) { Total += amount; Changed?.Invoke(Total); }
}

Expected output

changed:2
6
1
Try this
Unsubscribe the same handler identity, and remember that ordinary event invocation is synchronous.