Published Build3 min read
The Event Switch Is a Lookup Table You Volunteered to Maintain
A dev.to post argues that the type switch at the heart of every C# event pipeline is dynamic dispatch written longhand, and that one cast to dynamic hands the job back to the runtime.
Written for builders.See today for builders

What happened
- A dev.to post titled 'Delete the Event Switch: Runtime Dispatch with dynamic in C#' by hiteshk97 argues that a switch over event types is dynamic dispatch: 'You just wrote it by hand, took on the maintenance yourself, and skipped the help the runtime was ready to give you.'
- The post's prescription is to cast to dynamic at a single boundary and let the C# runtime binder pick the overload based on the argument's actual runtime type.
- The example switch is a HandleAsync(BaseEvent) method with arms for OrderPlaced, PaymentFailed and InventoryReserved, each forwarding to a HandleAsync overload, plus a default arm throwing NotSupportedException with the message "No handler for '{@event.GetType().Name}'."
- The post describes the switch as 'a lookup table you maintain by hand, keyed on runtime type, mapping each type to the method that should run', and says that is 'the exact job overload resolution already does for you. The compiler does it for free, at every call site, and it never forgets to add a case.'
- The post states that in the switch version the pattern-match arm and the target method are 'saying the same thing twice', and that the type check is the dispatch written out longhand.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A post on dev.to by hiteshk97 makes a narrow and testable claim about C# event-driven code: the switch over event types that sits in every such codebase is dynamic dispatch written by hand, with the maintenance burden taken on voluntarily [1]. Its prescription is one cast to `dynamic` at a single boundary, letting the C# runtime binder pick the overload from the argument's actual runtime type [2].
The setup is familiar. A `HandleAsync(BaseEvent)` switches on `OrderPlaced`, `PaymentFailed` and `InventoryReserved`, each arm forwarding to the matching overload, with a default arm throwing `NotSupportedException` [3]. The post's reading of that method is the useful bit: it is a lookup table keyed on runtime type, mapping each type to the method that should run, which is the job overload resolution already does at every call site, and the compiler never forgets to add a case [4].
The duplication is the actual defect. In the switch version, the pattern-match arm and the target method state the same routing fact twice [5]. With three event types that is six places where the routing is written, against three in the overload version [16]. Add an event and you edit the record and the handler and the central switch; in the refactor you add the record and one `Dispatch` overload, with nothing central to touch [7].
The refactor itself is small: `HandleAsync` null-checks and returns `Dispatch((dynamic)@event)`, and the private `Dispatch` overloads do the work [6]. Two details carry the weight.
First, the fallback. A least-specific `Dispatch(BaseEvent)` overload throws `NotSupportedException` naming the runtime type [8]. Skip it and an unrecognised event throws `RuntimeBinderException` from inside the framework, an error the post describes as confusing and not naming your domain [9]. It works because every event is a `BaseEvent`, so the binder always has at least that overload and picks the most-derived one that fits [10]. Note what this is and is not: the post says it gives the same guarantee the `_` arm gave you, now expressed as an overload [11]. The unhandled event still fails at runtime. What goes away is the drift between two copies of the same decision, not the failure mode.
Second, naming. The boundary is `HandleAsync` and the overload set is `Dispatch` because a `Dispatch(BaseEvent)` boundary would collide with the fallback: identical signatures are a compile error, and `public` versus `private` does not break the tie [12].
On cost, the post's account is a one-time hit the first time a given runtime type crosses the boundary while the DLR builds the call site and resolves the overload, cached per runtime type thereafter, with warm dispatch described as about as fast as a normal virtual call and no per-event reflection [13]. It also tells you to benchmark against a `Dictionary<Type, Func<...>>` on a genuinely hot path handling millions of events a second [14]. That is a single author's characterisation, and the excerpt cuts off mid-sentence before the alternatives are spelled out [17].
What to watch: the scope condition. The post applies this to a small, closed hierarchy of domain events that you own [15]. Events arriving from outside that hierarchy, or handlers spread across types rather than sitting in one overload set, are not what was demonstrated. Before adopting it, measure the warm path yourself and confirm the fallback overload exists, because without it the pattern trades a clear exception for an opaque one [8][9].
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
A dev.to post titled 'Delete the Event Switch: Runtime Dispatch with dynamic in C#' by hiteshk97 argues that a switch over event types is dynamic dispatch: 'You just wrote it by hand, took on the maintenance yourself, and skipped the help the runtime was ready to give you.'
- [2]
The post's prescription is to cast to dynamic at a single boundary and let the C# runtime binder pick the overload based on the argument's actual runtime type.
- [3]
The example switch is a HandleAsync(BaseEvent) method with arms for OrderPlaced, PaymentFailed and InventoryReserved, each forwarding to a HandleAsync overload, plus a default arm throwing NotSupportedException with the message "No handler for '{@event.GetType().Name}'."
ReportedView cited source - [4]
The post describes the switch as 'a lookup table you maintain by hand, keyed on runtime type, mapping each type to the method that should run', and says that is 'the exact job overload resolution already does for you. The compiler does it for free, at every call site, and it never forgets to add a case.'
- [5]
The post states that in the switch version the pattern-match arm and the target method are 'saying the same thing twice', and that the type check is the dispatch written out longhand.
ReportedView cited source - [6]
The refactored EventProcessor exposes a public HandleAsync(BaseEvent) that calls ArgumentNullException.ThrowIfNull(@event) and returns Dispatch((dynamic)@event), with private Dispatch overloads for OrderPlaced, PaymentFailed and InventoryReserved.
ReportedView cited source
Sources & coverage · 1 publisher
The reporting this story was synthesized from, earliest first. Every link goes to the original.
- dev.toHiteshAug 12Delete the Event Switch: Runtime Dispatch with dynamic in C#
Cited in this coverage: hiteshk97, dev.to
