You've been implementing this pattern since lesson 105. This lesson just gives it its name.
Lesson 105 taught you the event keyword — a controlled way for a class to announce "something happened" to whoever's interested. Lesson 106 built the full standard event pattern: custom EventArgs, a protected virtual OnXxx raiser, multiple subscribers reacting to one PriceChanged or OrderShipped event. Lesson 192 took that further with an EventAggregator, decoupling publishers and subscribers so neither knows the other exists.
Every single one of those lessons was teaching you a concrete C# implementation of a pattern with a formal name: the Observer Pattern — a Behavioral pattern from the design patterns lesson earlier in this Part. You already know how to use it. This lesson connects the dots: the classic GoF shape behind what you've been writing, and where else in .NET the exact same idea shows up.
By the end, you'll know the textbook Subject/Observer terminology, see precisely how C#'s event keyword implements it, and recognize IObservable<T>/IObserver<T> — the interfaces behind Rx.NET — as yet another concrete implementation of the same idea.
The Observer Pattern defines a one-to-many relationship between objects: when one object's state changes, all its registered dependents are notified automatically. The object being watched doesn't need to know anything specific about who's watching — just that a list of watchers exists and should be told when something happens.
The original GoF formulation names two roles:
Attach/Subscribe and Detach/UnsubscribeNotify() on every registered Observer when its own state changesUpdate()-style method the Subject can callC#'s event keyword IS the Observer pattern, built into the language. The class declaring public event EventHandler<PriceChangedEventArgs>? PriceChanged; is the Subject. Every method that does someObject.PriceChanged += HandlePriceChanged; is registering an Observer. The multicast delegate's invocation list (lesson 097) IS the Subject's list of registered Observers. You've been building Subject/Observer relationships since lesson 105 — just using C#'s own vocabulary (event, +=, EventHandler) instead of the GoF vocabulary (Subject, Attach, Notify).
Without Observer, a class that needs to react to something happening elsewhere has two bad options: poll repeatedly to check "has it happened yet?", or the source object has to directly call every interested class by name — coupling it to every consumer it happens to know about today:
// WITHOUT Observer — the order-shipping class directly calls
// every interested class by name, hard-coded, one by one
public sealed class OrderShippingService
{
private readonly EmailNotifier _emailNotifier;
private readonly AnalyticsLogger _analyticsLogger;
private readonly InventorySync _inventorySync;
public void MarkShipped(Order order)
{
// Every new "interested party" means editing THIS method directly —
// OrderShippingService is now coupled to every consumer it knows about.
_emailNotifier.SendShippedEmail(order);
_analyticsLogger.LogShipment(order);
_inventorySync.ReleaseReservedStock(order);
}
}
public interface IObserver
{
void Update(Order order);
}
public class OrderSubject
{
private readonly List<IObserver> _observers = [];
public void Attach(IObserver o) => _observers.Add(o);
public void Notify(Order order)
{
foreach (var o in _observers)
o.Update(order);
}
}
public class OrderShippingService
{
public event EventHandler<OrderShippedEventArgs>? OrderShipped;
protected virtual void OnOrderShipped(OrderShippedEventArgs e) =>
OrderShipped?.Invoke(this, e);
public void MarkShipped(Order order) =>
OnOrderShipped(new OrderShippedEventArgs(order.TrackingNumber));
}
These are the same pattern. _observers.Add(o) and OrderShipped += handler both register a watcher. foreach (var o in _observers) o.Update(order) and OrderShipped?.Invoke(this, e) both notify every registered watcher. C# just gives you the entire Subject bookkeeping — the list, the add/remove, the null-safe iteration — for free, via the compiler-generated multicast delegate behind every event field (lesson 097).
OrderShippingService — owns the OrderShipped event field, decides when to raise itshippingService.OrderShipped += HandleShipped; — registers an Observer, exactly like _observers.Add(o)void HandleShipped(object? sender, OrderShippedEventArgs e) — this is your Observer's Update()OnOrderShipped calling OrderShipped?.Invoke(this, e) walks the invocation list and calls every registered handler — this IS the Subject's Notify() loop, generated by the compilershippingService.OrderShipped -= HandleShipped; — removes the Observer, preventing the memory leak covered in lesson 106public sealed class OrderShippedEventArgs(string trackingNumber) : EventArgs
{
public string TrackingNumber { get; } = trackingNumber;
}
public sealed class OrderShippingService // ← the Subject
{
public event EventHandler<OrderShippedEventArgs>? OrderShipped;
private void OnOrderShipped(OrderShippedEventArgs e) => OrderShipped?.Invoke(this, e);
public void MarkShipped(string trackingNumber) =>
OnOrderShipped(new OrderShippedEventArgs(trackingNumber));
}
// ── Three independent Observers, none aware the others exist ──
var shipping = new OrderShippingService();
shipping.OrderShipped += (_, e) => Console.WriteLine($"[Email] Shipped: {e.TrackingNumber}");
shipping.OrderShipped += (_, e) => Console.WriteLine($"[Analytics] Logged: {e.TrackingNumber}");
shipping.OrderShipped += (_, e) => Console.WriteLine($"[Inventory] Released stock for: {e.TrackingNumber}");
shipping.MarkShipped("1Z999AA10123456784");
// All three handlers run — OrderShippingService never mentions Email, Analytics,
// or Inventory by name. That decoupling IS the Observer pattern working.
Code → Meaning → Result: Three completely unrelated Observers subscribe to one Subject. OrderShippingService knows nothing about email, analytics, or inventory — it only knows it has a list of delegates to invoke. Add a fourth Observer tomorrow, and OrderShippingService doesn't change at all.
Lesson 192's EventAggregator is Observer taken one step further: instead of subscribing directly to one specific Subject instance, publishers and subscribers both depend only on IEventAggregator — a shared, decoupled "message bus" that plays Subject for the whole application:
// From 192 — the SAME Subject/Observer roles, generalized across the whole app:
public interface IEventAggregator
{
void Publish<TEvent>(TEvent evt); // Notify() — announce to all Observers
void Subscribe<TEvent>(Action<TEvent> handler); // Attach() — register an Observer
}
// AnalyticsModule and NotificationModule both Subscribe to the SAME event type,
// without ever referencing OrderFulfillmentService (the publisher) directly —
// and OrderFulfillmentService never references either subscriber directly.
// This is Observer, decoupled even further: publisher and subscriber depend
// only on the shared bus, not on each other.
Where a plain C# event field ties Observers directly to one specific Subject instance, an event aggregator generalizes the Subject role into a shared bus — but the underlying relationship (state change → notify a list of interested parties) is identical.
A YouTube channel (the Subject) doesn't know your name, your email, or anything about you specifically — it just maintains a list of subscribers. When it uploads a new video (a state change), every subscriber (Observer) gets notified, automatically, without the channel ever calling each subscriber by name. You can subscribe or unsubscribe at any time, and the channel's behavior — uploading videos — never changes because of who happens to be watching.
The .NET base class library also defines Observer directly as a pair of generic interfaces — IObservable<T> and IObserver<T> — which are the foundation Reactive Extensions (Rx.NET) is built on:
IDisposable Subscribe(IObserver<T> observer) — registering an Observer returns a handle you can dispose to unsubscribeOnNext(T value) (a new item arrived), OnError(Exception e), OnCompleted() — richer than a plain event, because it can represent an entire stream ending, not just one notificationThis lesson won't teach Rx.NET's operators or reactive programming techniques — that's a large, separate topic outside this lesson's scope. The point here is purely vocabulary: recognize IObservable<T>/IObserver<T> as a third concrete .NET implementation of the exact same Subject/Observer relationship you already know from event, just with a richer, stream-oriented contract instead of a single notification.
event, am I not really using the Observer pattern?"You are — this is the single most important point of this lesson. GoF's Subject/Observer terminology predates C#'s event keyword by years; C# effectively baked the pattern directly into the language, with compiler-generated multicast delegate bookkeeping standing in for the hand-written List<IObserver>. Using event isn't "instead of" Observer — it IS Observer, expressed with first-class language support instead of a hand-rolled interface.
Both involve one class holding a reference to something interchangeable, but the shape is different. Strategy (241) is one-to-one: a context holds exactly one algorithm and calls it to get a result. Observer is one-to-many: a Subject holds a list of Observers and notifies all of them, expecting no return value back — it's a broadcast, not a request for an answer.
A short-lived Observer subscribes to a long-lived Subject's event and never unsubscribes — the Subject's invocation list keeps a live reference to the Observer forever, preventing garbage collection. Always pair += with a corresponding -= when the Observer's lifetime is shorter than the Subject's — lesson 106 covers this in full depth.
event already does the job Writing a custom IObserver interface and a hand-rolled List<IObserver> Subject class in modern C#, duplicating exactly what event already gives you for free. Reach for C#'s built-in event keyword for ordinary in-process notifications — it's the idiomatic, battle-tested implementation of this pattern in C#, and reinventing it rarely earns its cost.
Relying on multiple subscribers to a single event running in a specific order, or assuming one handler throwing an exception won't stop the others from running — a plain multicast delegate invokes handlers sequentially in subscription order, and an unhandled exception in one handler will, by default, prevent later handlers in the same invocation from running. If handler independence or error isolation matters, that needs to be built deliberately (e.g., wrapping each handler call in its own try/catch, or using a dedicated event aggregator that isolates failures) — it isn't automatic just because you're "using Observer."
event for straightforward, in-process, one-Subject notifications — it's the right default almost every time.IObservable<T>/Rx.NET specifically when you need to compose, filter, or combine streams of notifications over time — genuinely out of scope for this introductory-level lesson, but worth knowing it exists for that purpose.And when it's overkill: a direct method call is simpler and clearer than an event when there's exactly one fixed, always-present consumer that will never change — Observer's value comes specifically from not knowing (or needing to care) who's listening.
event field and decides when to raise it.+=, waiting to be notified.+= / ?.Invoke(this, e) / -=.event IS the pattern, not a substitute for it.IObservable<T>/IObserver<T> (the base of Rx.NET) is a second, richer, stream-oriented BCL implementation of the same idea.event keyword and multicast delegates ARE a built-in implementation of Observer — += is Attach, ?.Invoke(this, e) is Notify, -= is Detach.EventAggregator generalizes the Subject role into a shared bus, decoupling publishers and subscribers even further.IObservable<T>/IObserver<T> — the base of Rx.NET — is a second, richer BCL implementation of the same relationship, for streams of values over time.You've connected C#'s event keyword to its formal GoF name. Let's confirm you can map the vocabulary both ways.
1. In classic GoF terms, what role does a class play when it declares public event EventHandler<OrderShippedEventArgs>? OrderShipped;?
Correct: B
Why B is correct: The class declaring and raising the event maintains the (compiler-generated) list of registered handlers and notifies them on state change — exactly the Subject's role.
Why A is incorrect: The Observer role belongs to whatever subscribes with +=, not to the class declaring the event.
Why C is incorrect: Strategy is a one-to-one, return-a-result pattern (lesson 241) — unrelated to the one-to-many broadcast shape of an event declaration.
Why D is incorrect: Adapter converts between interface shapes (243) — declaring an event doesn't translate any interface.
Reinforcement: The class that owns the event field and decides when to raise it is always the Subject.
2. Which C# operation corresponds to the classic GoF Subject's "Attach" operation?
Correct: B
Why B is correct: Using += to subscribe a handler registers an Observer with the Subject — precisely what "Attach" means in the classic GoF Subject/Observer shape.
Why A is incorrect: Constructing an object has nothing to do with registering as an Observer.
Why C is incorrect: This is the Subject's "Notify" step — invoking the invocation list, not registering into it.
Why D is incorrect: This is an unrelated async delay, with no connection to Observer registration.
Reinforcement: += is Attach, ?.Invoke(...) is Notify, -= is Detach — the full mapping between C# syntax and GoF vocabulary.
3. A developer says: "Since I'm just using C#'s event keyword, I'm not really using a design pattern — Observer is something you'd only get by hand-writing a Subject class and an IObserver interface." Is this correct?
Correct: B
Why B is correct: This is the lesson's central point in Common Confusion — GoF's terminology predates C#'s event keyword, which bakes the exact same Subject/Observer relationship directly into the language.
Why A is incorrect: This treats a specific historical implementation technique as the definition of the pattern, rather than the underlying relationship (one-to-many notification) the pattern actually describes.
Why C is incorrect: A language keyword or built-in feature can absolutely implement a design pattern's shape — that's exactly what event does.
Why D is incorrect: Strategy is a different, one-to-one pattern (241) — event implements the one-to-many broadcast shape of Observer, not Strategy.
Reinforcement: A pattern is defined by its structural intent, not by which specific syntax happens to express it.
4. What distinguishes IObservable<T>/IObserver<T> from a plain C# event, while both still being implementations of the same underlying pattern?
Correct: B
Why B is correct: The "Under the Hood" section describes exactly this — IObserver<T>'s three methods can represent an entire stream, including completion and errors, which a single-notification event doesn't natively express.
Why A is incorrect: The lesson explicitly frames IObservable<T>/IObserver<T> as a third concrete .NET implementation of the same Subject/Observer relationship, not an unrelated pattern.
Why C is incorrect: IObservable<T>.Subscribe returns an IDisposable specifically so callers CAN unsubscribe, by disposing it.
Why D is incorrect: A plain event's multicast delegate already supports many subscribers, exactly as this lesson's three-Observer example demonstrated.
Reinforcement: Both are genuine Observer implementations — they differ in the richness of what a single "notification" can represent, not in the underlying one-to-many relationship.
You already know how to use this pattern — now you know its name, its classic textbook shape, and where else in the .NET ecosystem the same idea shows up.
dotnetmadeeasy.com — Learn C# and .NET, the right way.