// Exercise 3: a resilient event bus (hard) // // A plain multicast delegate stops at the first throwing handler and keeps // subscribers alive forever. Build an EventBus that fixes both: // // - Subscribe(Action handler): returns an IDisposable; disposing it // unsubscribes that exact handler (no need to keep the lambda around). // Disposing twice must be harmless. // - Publish(T message): invokes every handler registered for exactly // type T, in subscription order. If handlers throw, ALL handlers still // run, and Publish then throws an AggregateException with every failure. // - It must be safe for a handler to unsubscribe ITSELF (or subscribe a // new handler) while Publish is iterating — snapshot the list first. // - HandlerCount() for the checks. // // Run with: dotnet run (inside a console project). using System; using System.Collections.Generic; class EventBus { // TODO: e.g. Dictionary> #pragma warning disable IDE0060 public IDisposable Subscribe(Action handler) { // TODO return new Unsubscriber(() => { }); } public void Publish(T message) { // TODO } public int HandlerCount() => 0; // TODO #pragma warning restore IDE0060 private sealed class Unsubscriber : IDisposable { private Action? _onDispose; public Unsubscriber(Action onDispose) => _onDispose = onDispose; public void Dispose() { _onDispose?.Invoke(); _onDispose = null; } } } record UserCreated(string Name); record UserDeleted(string Name); class Program { static void Main() { int pass = 0, total = 0; var bus = new EventBus(); var log = new List(); var s1 = bus.Subscribe(e => log.Add("welcome " + e.Name)); bus.Subscribe(e => throw new InvalidOperationException("mailer down")); bus.Subscribe(e => log.Add("audit " + e.Name)); bus.Subscribe(e => log.Add("bye " + e.Name)); total++; try { bus.Publish(new UserCreated("ana")); Console.WriteLine("FAIL: expected AggregateException"); } catch (AggregateException agg) when (agg.InnerExceptions.Count == 1) { if (string.Join("|", log) == "welcome ana|audit ana") { Console.WriteLine("PASS: all handlers ran despite a failure"); pass++; } else Console.WriteLine("FAIL: log = " + string.Join("|", log)); } total++; s1.Dispose(); s1.Dispose(); if (bus.HandlerCount() == 2 && bus.HandlerCount() == 1) { Console.WriteLine("PASS: dispose unsubscribes exactly once"); pass++; } else Console.WriteLine($"FAIL: counts {bus.HandlerCount()}/{bus.HandlerCount()}"); total++; var bus2 = new EventBus(); int calls = 0; IDisposable? self = null; self = bus2.Subscribe(_ => { calls++; self!.Dispose(); }); bus2.Subscribe(_ => calls++); try { bus2.Publish(1); bus2.Publish(2); if (calls == 3) { Console.WriteLine("PASS: self-unsubscribe during publish"); pass++; } else Console.WriteLine("FAIL: calls = " + calls); } catch (InvalidOperationException e) { Console.WriteLine("FAIL: collection modified during publish: " + e.Message); } Console.WriteLine($"--- {pass}/{total} checks passed ---"); } }