CodeGym /Courses /C# SELF /Subscribers and safe event invocation

Subscribers and safe event invocation

C# SELF
Level 53 , Lesson 2
Available

1. Introduction

When we write

worker.WorkCompleted += listener.OnWorkCompleted;

we're actually adding a pointer to a method into the event's "invocation list" (multicast delegate). That "list" inside the event is just a sequence of methods that will be called when the event is raised. In C# an event is implemented on top of a delegate that supports multiple subscribers.

Think of it like a mailing list: you have a list of subscribers (email addresses). When you send the mail (raise the event), all subscribers get the message. If someone unsubscribes, they're removed from the list and stop receiving messages.

How to add or remove a subscriber

Subscription (+=) and unsubscription (-=) operate on the delegate inside the event. Here's an example with a lambda that you can both subscribe and unsubscribe:

EventHandler<WorkCompletedEventArgs> handler = (sender, e) =>
{
    Console.WriteLine($"[Lambda] Work completed: {e.Message}");
};

worker.WorkCompleted += handler; // Subscribe
worker.WorkCompleted -= handler; // Unsubscribe

For normal methods unsubscription looks the same:

worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted -= listener.OnWorkCompleted;

Note: if you subscribed the same method multiple times, it will be called that many times and you need to call -= the same number of times to remove it.

2. Why manage subscriptions manually at all?

Why is it important to manage subscribers?

In real applications, especially long-lived ones (like desktop or server apps), improper subscription management can lead to memory leaks. If a subscriber object is no longer needed but still "hangs" in the event's subscriber list, it won't be collected by the GC — because there's still a reference to it from the event delegate.

Illustration

Action Result for subscriber
+= (subscribed) Added to the list
-= (unsubscribed) Removed from the list
Subscriber object deleted If NOT unsubscribed! — NOT collected, because there's still a reference in the event
Subscriber object deleted If UNsubscribed — will be collected normally

How to find out who is subscribed to an event?

Events encapsulate the subscriber list, so from outside the publisher class you can't get that list directly — you can only add (+=) or remove (-=) handlers.

However inside the class where the event is declared on a delegate (for example EventHandler), you can get the current subscriber list using GetInvocationList():

// Inside the publisher class
if (WorkCompleted != null)
{
    foreach (Delegate subscriber in WorkCompleted.GetInvocationList())
    {
        Console.WriteLine($"Handler: {subscriber.Method.Name}, Target: {subscriber.Target}");
    }
}

This trick is rarely needed in day-to-day development, but can be useful for debugging or implementing bulk unsubscription of all subscribers.

3. Safe event invocation: "mines" and how to avoid them

What can go wrong when raising an event?

It looks simple: you call

WorkCompleted?.Invoke(this, args);

and everything works... most of the time! But there are subtleties. Here they are:

1. Multithreading hazard

In a multithreaded app you can get into a situation where between checking the event for null and calling the handlers another thread changes subscriptions. For example:

1) Thread A checks: WorkCompleted != null.
2) Meanwhile thread B unsubscribes from the event (-=), and the handler list becomes empty.
3) Thread A tries to call WorkCompleted.Invoke(...) — a NullReferenceException occurs because there are no handlers anymore.

This is a classic data race when working with events.

2. Unexpected exceptions in handlers

If one of the subscribers throws an exception while handling the event, the call to the remaining handlers is interrupted. In other words the event "breaks" on the first exception, and the remaining subscribers don't get notified. To avoid this, it's recommended to wrap each handler call in a try-catch if it's important that everyone gets the signal.

3. Unwanted retention of context

An event handler is often an instance method that captures a reference to the subscriber object (this). If the subscriber forgets to unsubscribe from the publisher, a reference to it is kept in the publisher's delegate list. As a result the garbage collector cannot free that object — you get a memory leak.

How to safely raise an event?

1) Copy the delegate to a local variable

Invoking via a local variable guarantees that the subscriber list won't change during the call:

// The old reliable way
var handler = WorkCompleted;
if (handler != null)
{
    handler(this, args);
}

Or more modern, with the null-conditional operator:

WorkCompleted?.Invoke(this, args);

In most cases this is enough, because the C# compiler "understands" this construct and does an internal copy of the reference (see official docs).

2) Protect against exceptions in handlers

If it's critical that all handlers are invoked (even if one fails), iterate manually:

var handler = WorkCompleted;
if (handler != null)
{
    foreach (EventHandler<WorkCompletedEventArgs> subscriber in handler.GetInvocationList())
    {
        try
        {
            subscriber(this, args);
        }
        catch (Exception ex)
        {
            // Log, but don't let the whole event "crash"
            Console.WriteLine($"Error in handler: {ex.Message}");
        }
    }
}

This approach is rarely needed for regular UI scenarios, but is useful in libraries, loggers and complex systems.

3) Prevent memory leaks

If a subscriber "lives" shorter than the publisher (for example, a window subscribed to an application-level event), it must unsubscribe:

worker.WorkCompleted -= listener.OnWorkCompleted;

Otherwise the garbage collector won't be able to free listener, even if there are no more "explicit" references to it.

4. Practical example: bulk subscription and unsubscription manager

Let's expand our learning app. Imagine we have several listeners — and we want to dynamically subscribe and remove them as the program runs.

public class WorkListener
{
    private readonly string _name;

    public WorkListener(string name)
    {
        _name = name;
    }

    public void OnWorkCompleted(object sender, WorkCompletedEventArgs e)
    {
        Console.WriteLine($"Listener {_name}: {e.Message}");
    }
}

In the main program:

var worker = new Worker();

var listeners = new List<WorkListener>
{
    new WorkListener("Ivan"),
    new WorkListener("Maria"),
    new WorkListener("Denis")
};

// Subscribe all listeners
foreach (var listener in listeners)
    worker.WorkCompleted += listener.OnWorkCompleted;

// Raise the event
worker.DoWork();

// Bulk unsubscribe
foreach (var listener in listeners)
    worker.WorkCompleted -= listener.OnWorkCompleted;

// Verify nobody reacts anymore
worker.DoWork();

In the console after the first work run you'll see 3 messages, after the second — none.

5. Tips for safe event usage

  • Unsubscribe in time if the subscriber's lifetime is shorter than the publisher's.
  • If you implement the "long-lived publisher — short-lived subscriber" pattern, always unsubscribe, for example in Dispose(), when closing a window or otherwise explicitly ending the object's life.
  • For one-shot events you can use an anonymous lambda handler and unsubscribe inside it immediately:
EventHandler<WorkCompletedEventArgs> handler = null;
handler = (s, e) => 
{
    Console.WriteLine("Event handled once!");
    worker.WorkCompleted -= handler;
};
worker.WorkCompleted += handler;
  • Don't keep extra references to subscribers or handlers to check "who is subscribed" — that's not needed in normal business logic. Do it only for debugging purposes.

6. Common mistakes and how to avoid them

Mistake #1: forgot to unsubscribe from the event — memory leak.
If a subscriber didn't unsubscribe, especially in big apps with many events and subscribers, objects can stay in memory longer than needed. This bug often doesn't show up immediately, but leads to increased memory use and degraded performance.

Mistake #2: raising an event without checking for null.
If an event has no subscribers and you try to call it directly, you'll get a NullReferenceException. Newer C# versions help with the null-conditional operator ?., but if you work with old code or iterate handlers manually, don't forget to check the event for null.

Mistake #3: an exception in one handler stops the rest from being called.
If one handler throws an exception, subsequent handlers won't be invoked. If it's important that all subscribers get notified, iterate the handlers in a loop and wrap each call in a try/catch.

2
Task
C# SELF, level 53, lesson 2
Locked
Safe event invocation with exception handling
Safe event invocation with exception handling
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION