Event Subscription and Management
Final section of events and delegates for now. We have previously seen how delegates and events work. Events are nothing without one or more subscribers.
In this final post I will show you how to subscribe to events, manage subscriptions, and how you can avoid common pitfalls that in most cases lead to memory leaks. By the end you will know how the publisher -subscriber pattern works and how you can implement that in a .NET application.
The Publisher-Subscriber Pattern
I won't deep dive into this pattern theoretically. I will keep it short and then focus on how to implement it instead. The publisher-subscriber (often referred to as pub-sub) is a messaging pattern where you three primary thing.
- The publisher is the one responsible for raising events. This could be a worker or an order processor, etc...
- A subscriber that will listen to and act/handle events that has been raised by the publisher.
- And finally we have the event, which can also be seen as an internal notification mechanism between the publisher and the subscriber.
One thing I really like about this pattern is that it promotes loose coupling. Why? Publishers won't know a thing about who their subscribers are, and the subscribers don't know anything about the implementation of the publishers.
Alright - let's get to the fun part and write some code. I will start with the basics and advance along the way all to the bottom of this post.
Basic Event Subscription
Let's start out with a basic example. This is a task manager with a task logger as the subscriber.
public class TaskManager
{
public event EventHandler<TaskEventArgs>? TaskCompleted;
public void CompleteTask(string taskName)
{
Console.WriteLine($"Completing task: {taskName}");
TaskCompleted?.Invoke(this, new TaskEventArgs(taskName));
}
}
public class TaskEventArgs : EventArgs
{
public string TaskName { get; }
public TaskEventArgs(string taskName) => TaskName = taskName;
}
// Subscriber
public class TaskLogger
{
public void Subscribe(TaskManager manager)
{
// Subscribe using += operator
manager.TaskCompleted += OnTaskCompleted;
}
private void OnTaskCompleted(object? sender, TaskEventArgs e)
{
Console.WriteLine($" [Logger] Task logged: {e.TaskName}");
}
}
// Usage
var manager = new TaskManager();
var logger = new TaskLogger();
logger.Subscribe(manager);
manager.CompleteTask("Deploy Application");
/* Output:
Completing task: Deploy Application
[Logger] Task logged: Deploy Application
*/There is not much to say about it. It has a manager responsible for managing the tasks, and then we have the subscriber that will subscribe to the manager and handle events it will publish.
Multiple Subscribers
A cool feature of events is that multiple subscribers can listen for the same event. This gives us the option to do multiple different things for a single event, like shown below.
public class EmailNotifier
{
public void Subscribe(TaskManager manager)
{
manager.TaskCompleted += (sender, e) =>
{
Console.WriteLine($" [Email] Notification sent for: {e.TaskName}");
};
}
}
public class MetricsCollector
{
public void Subscribe(TaskManager manager)
{
manager.TaskCompleted += (sender, e) =>
{
Console.WriteLine($" [Metrics] Recorded completion of: {e.TaskName}");
};
}
}
// Usage
var manager = new TaskManager();
var logger = new TaskLogger();
var emailer = new EmailNotifier();
var metrics = new MetricsCollector();
logger.Subscribe(manager);
emailer.Subscribe(manager);
metrics.Subscribe(manager);
manager.CompleteTask("Database Backup");
/* Output:
Completing task: Database Backup
[Logger] Task logged: Database Backup
[Email] Notification sent for: Database Backup
[Metrics] Recorded completion of: Database Backup
*/A simple example of multiple subscribers for a single event. As you can see we could be sending emails to the admin team, and update maybe Prometheus with some metrics once this events was raised.
Why It Is Important To Unsubscribe
When you subscribe to an event, the publisher will hold a reference to the subscriber. If you do not unsubscribe, the subscriber can never be garbage collected, even if you are done using it. This will lead to a memory leak!
Let's take a look at how we successfully can unsubscribe from the event again.
public class TaskMonitor
{
private TaskManager _manager;
public TaskMonitor(TaskManager manager)
{
_manager = manager;
}
public void Subscribe()
{
Console.WriteLine(" β Subscribing to events");
_manager.TaskCompleted += OnTaskCompleted;
}
public void Unsubscribe()
{
Console.WriteLine(" β Unsubscribing from events");
_manager.TaskCompleted -= OnTaskCompleted; // Important!
}
private void OnTaskCompleted(object? sender, TaskEventArgs e)
{
Console.WriteLine($" [Monitor] Tracked: {e.TaskName}");
}
// Finalizer to clean up
~TaskMonitor()
{
Unsubscribe();
}
}
// Usage demonstrating proper cleanup
var manager = new TaskManager();
var monitor = new TaskMonitor(manager);
monitor.Subscribe();
manager.CompleteTask("First Task");
monitor.Unsubscribe();
manager.CompleteTask("Second Task"); // Monitor won't receive this
/* Output:
β Subscribing to events
Completing task: First Task
[Monitor] Tracked: First Task
β Unsubscribing from events
Completing task: Second Task
*/How To Store Event Handlers for Correct Unsubscription
If or when you are using a lambda expression or an anonymous method, you would need to store a reference somewhere to unsubscribe from the event correctly. Let's see how we could accomplish that.
public class NotificationService
{
private TaskManager _manager;
private EventHandler<TaskEventArgs> _taskHandler;
public NotificationService(TaskManager manager)
{
_manager = manager;
// Store the handler reference
_taskHandler = (sender, e) =>
{
Console.WriteLine($" [Notification] {e.TaskName} completed!");
};
}
public void Subscribe()
{
Console.WriteLine(" β Subscribing");
_manager.TaskCompleted += _taskHandler;
}
public void Unsubscribe()
{
Console.WriteLine(" β Unsubscribing");
_manager.TaskCompleted -= _taskHandler; // Works because we stored it
}
}
// Usage
var manager = new TaskManager();
var notifier = new NotificationService(manager);
notifier.Subscribe();
manager.CompleteTask("Build Project");
notifier.Unsubscribe();
manager.CompleteTask("Run Tests"); // Notifier won't receive thisThe Problem With Lambda Expressions
A word of warning. An inline lambda expression cannot be unsubscribed. Just saying it π (an advice from a friend of mine)
var manager = new TaskManager();
// β PROBLEM - Cannot unsubscribe
manager.TaskCompleted += (sender, e) =>
{
Console.WriteLine($"Handled: {e.TaskName}");
};
// This does NOT work - creates a new lambda, doesn't remove the old one
manager.TaskCompleted -= (sender, e) =>
{
Console.WriteLine($"Handled: {e.TaskName}");
};
// β
SOLUTION - Store the handler
EventHandler<TaskEventArgs> handler = (sender, e) =>
{
Console.WriteLine($"Handled: {e.TaskName}");
};
manager.TaskCompleted += handler;
// Later...
manager.TaskCompleted -= handler; // This works! πOkay, now that we have seen how to unsubscribe from events of different kinds, I think it is time to bring it all together and build something that makes sense.
Building A Stock Trading System With Events
Below is the code for a system that implement event management for a stock trading system. Who doesn't like money?! π° Let's split it into multiple sections.
EventArgs
public class StockPriceChangedEventArgs : EventArgs
{
public string Symbol { get; }
public decimal OldPrice { get; }
public decimal NewPrice { get; }
public decimal Change => NewPrice - OldPrice;
public decimal ChangePercent => (Change / OldPrice) * 100;
public StockPriceChangedEventArgs(string symbol, decimal oldPrice, decimal newPrice)
{
Symbol = symbol;
OldPrice = oldPrice;
NewPrice = newPrice;
}
}Publisher
public class StockMarket
{
public event EventHandler<StockPriceChangedEventArgs>? PriceChanged;
private Dictionary<string, decimal> _prices = new();
public void UpdatePrice(string symbol, decimal newPrice)
{
decimal oldPrice = _prices.GetValueOrDefault(symbol, newPrice);
_prices[symbol] = newPrice;
if (oldPrice != newPrice)
{
OnPriceChanged(symbol, oldPrice, newPrice);
}
}
protected virtual void OnPriceChanged(string symbol, decimal oldPrice, decimal newPrice)
{
var args = new StockPriceChangedEventArgs(symbol, oldPrice, newPrice);
PriceChanged?.Invoke(this, args);
}
}Subscribers
The system will have multiple subscribers, each having different behavior.
Subscriber 1 - Alert Service
public class AlertService
{
private StockMarket _market;
private EventHandler<StockPriceChangedEventArgs> _priceChangedHandler;
public AlertService(StockMarket market)
{
_market = market;
_priceChangedHandler = OnPriceChanged;
}
public void Subscribe()
{
Console.WriteLine(" [Alert Service] Subscribed");
_market.PriceChanged += _priceChangedHandler;
}
public void Unsubscribe()
{
Console.WriteLine(" [Alert Service] Unsubscribed");
_market.PriceChanged -= _priceChangedHandler;
}
private void OnPriceChanged(object? sender, StockPriceChangedEventArgs e)
{
if (Math.Abs(e.ChangePercent) > 5)
{
Console.WriteLine($" π¨ ALERT: {e.Symbol} moved {e.ChangePercent:F2}% " +
$"(${e.OldPrice} β ${e.NewPrice})");
}
}
~AlertService()
{
Unsubscribe();
}
}Subscriber 2 - The Trading Bot
public class TradingBot
{
private StockMarket _market;
private EventHandler<StockPriceChangedEventArgs> _priceChangedHandler;
public TradingBot(StockMarket market)
{
_market = market;
_priceChangedHandler = OnPriceChanged;
}
public void Subscribe()
{
Console.WriteLine(" [Trading Bot] Subscribed");
_market.PriceChanged += _priceChangedHandler;
}
public void Unsubscribe()
{
Console.WriteLine(" [Trading Bot] Unsubscribed");
_market.PriceChanged -= _priceChangedHandler;
}
private void OnPriceChanged(object? sender, StockPriceChangedEventArgs e)
{
if (e.Change > 0)
{
Console.WriteLine($" π€ BOT: {e.Symbol} trending up! Consider buying.");
}
else if (e.Change < 0)
{
Console.WriteLine($" π€ BOT: {e.Symbol} trending down! Consider selling.");
}
}
~TradingBot()
{
Unsubscribe();
}
}Subscriber 3 - Market Logger
public class MarketLogger
{
private StockMarket _market;
private EventHandler<StockPriceChangedEventArgs> _priceChangedHandler;
public MarketLogger(StockMarket market)
{
_market = market;
_priceChangedHandler = OnPriceChanged;
}
public void Subscribe()
{
Console.WriteLine(" [Market Logger] Subscribed");
_market.PriceChanged += _priceChangedHandler;
}
public void Unsubscribe()
{
Console.WriteLine(" [Market Logger] Unsubscribed");
_market.PriceChanged -= _priceChangedHandler;
}
private void OnPriceChanged(object? sender, StockPriceChangedEventArgs e)
{
Console.WriteLine($" π LOG: {DateTime.Now:HH:mm:ss} - {e.Symbol}: " +
$"${e.OldPrice:F2} β ${e.NewPrice:F2} " +
$"({e.ChangePercent:+0.00;-0.00}%)");
}
~MarketLogger()
{
Unsubscribe();
}
}Wirering it all up
public class StockTradingDemo
{
public static void Run()
{
var market = new StockMarket();
var alerts = new AlertService(market);
var bot = new TradingBot(market);
var logger = new MarketLogger(market);
// Subscribe all services
Console.WriteLine("=== Subscribing Services ===");
alerts.Subscribe();
bot.Subscribe();
logger.Subscribe();
Console.WriteLine("\n=== Price Updates ===");
market.UpdatePrice("AAPL", 150.00m);
market.UpdatePrice("AAPL", 155.00m); // +3.33%
market.UpdatePrice("AAPL", 165.00m); // +6.45% - triggers alert!
Console.WriteLine("\n=== Unsubscribing Alert Service ===");
alerts.Unsubscribe();
Console.WriteLine("\n=== More Price Updates ===");
market.UpdatePrice("GOOGL", 2800.00m);
market.UpdatePrice("GOOGL", 2600.00m); // -7.14% - alert service won't see this!
Console.WriteLine("\n=== Cleanup ===");
bot.Unsubscribe();
logger.Unsubscribe();
}
}The Result
=== Subscribing Services ===
[Alert Service] Subscribed
[Trading Bot] Subscribed
[Market Logger] Subscribed
=== Price Updates ===
π€ BOT: AAPL trending up! Consider buying.
π LOG: 14:30:15 - AAPL: $150.00 β $155.00 (+3.33%)
π€ BOT: AAPL trending up! Consider buying.
π LOG: 14:30:15 - AAPL: $155.00 β $165.00 (+6.45%)
π¨ ALERT: AAPL moved 6.45% ($155.00 β $165.00)
=== Unsubscribing Alert Service ===
[Alert Service] Unsubscribed
=== More Price Updates ===
π LOG: 14:30:15 - GOOGL: $2800.00 β $2600.00 (-7.14%)
π€ BOT: GOOGL trending down! Consider selling.
π LOG: 14:30:15 - GOOGL: $2600.00 β $2650.00 (+1.92%)
=== Cleanup ===
[Trading Bot] Unsubscribed
[Market Logger] UnsubscribedMy Top Advices for Event Subscriptions
Here are some of my top advices that I really recommend you to follow when you are working with event subscriptions.
Always Store Event Handlers When Using Lambdas
private EventHandler<EventArgs> _handler;
public void Subscribe()
{
_handler = (s, e) => HandleEvent();
publisher.Event += _handler;
}
public void Unsubscribe()
{
publisher.Event -= _handler;
}Implement IDisposable for Cleanup
public class EventSubscriber : IDisposable
{
private StockMarket _market;
private EventHandler<StockPriceChangedEventArgs> _handler;
public EventSubscriber(StockMarket market)
{
_market = market;
_handler = OnPriceChanged;
_market.PriceChanged += _handler;
}
private void OnPriceChanged(object? sender, StockPriceChangedEventArgs e)
{
// Handle event
}
public void Dispose()
{
_market.PriceChanged -= _handler;
}
}
// Usage with using statement
using (var subscriber = new EventSubscriber(market))
{
// Subscriber is active here
} // Automatically unsubscribed when disposedUse Weak Event Pattern for Long-Lived Publishers
If you have publishers in your application that will live longer than the subscribers, I would recommend you to use a weak reference to prevent a memory leak. This is advanced, but it is important to know about. An example would be an application-level singleton - that would probably live longer than your subscriber.
Be Careful with Lambda Expressions
// β You can't unsubscribe from this
publisher.Event += (s, e) => Console.WriteLine("Event!");
// β
A named method
publisher.Event += OnEvent;
void OnEvent(object? s, EventArgs e) => Console.WriteLine("Event!");
// β
A stored lambda
var handler = (object? s, EventArgs e) => Console.WriteLine("Event!");
publisher.Event += handler;Common Pitfalls and Solutions
Forgetting to Unsubscribe
The problem is that your subscribers never get garbage collected.
And the solution is to always unsubscribe in finalizers or Dispose() methods.
Subscribing Multiple Times
The problem is that your handler gets called multiple times per event.
The solution is to make sure you unsubscribe before subscribing, or check if already subscribed.
publisher.Event -= OnEvent; // Remove if exists
publisher.Event += OnEvent; // Add fresh subscription
Exception in Event Handler
Your problem is that if one subscriber throws an exception, subsequent subscribers don't get notified.
A solution is to make sure publishers should catch exceptions when invoking events (advanced technique).
Key Takeaways
This time listed as dots and not a plain text. Here are 7 things I would recommend you to write down by hand to memorize it both in your head and hand.
- Remember to always unsubscribe from events when you're done with them.
- Always store lambda expression references if you need to unsubscribe later.
- Make sure to use finalizers or
IDisposableto make sure you clean up properly. - A single event can have multiple subscribers.
- Using events will promote loose coupling in your application between the publishers and subscribers.
- Making inline lambda expressions cannot be unsubscribed from. Why? They will create a new instance each time they are invoked.
- Implementing the pub-sub pattern correctly, will make sure that your application is maintainable and can scale in the future.
You Made It!
Congratulations! You made it to the end of my events and delegates section. You should now have a good understanding of the following.
- Delegates are type-safe function pointers. We can use them to enable callbacks.
- Events are encapsulated delegates that makes the publisher-subscriber pattern possible.
- Subscription Management is a technique to make sure that you can subscribe, unsubscribe, and avoid memory leaks in your application.
What you just went trough is fundamental to C# and .NET. It runs everything from UI frameworks like WPF or WinForms to huge complex backends with multiple services, and reactive programming.
Additional Resources
Some good resources you should take your time to read, if you got it.


