Free tools Windows power users keep installed
One-click scans. No signup required.
dynamic can turn an event dispatcher into runtime overload resolution: pass an IEvent whose declared type is general, and the runtime binder selects the overload that matches the object’s actual type. That removes a type-pattern switch, but it does not remove complexity. Member lookup, overload selection, and some errors move from compilation to execution.
Use this technique when runtime types and overloads are an intentional part of the design. Keep a type-pattern switch when the event family is a known, closed set that readers should be able to see and verify in source. Neither approach is inherently faster; measure the real workload on the runtime you deploy.
As an Amazon Associate I earn from qualifying purchases.
What “delete the event switch” changes
Suppose an application receives several event implementations through one interface:
public interface IEvent { }
public sealed record UserRegistered(string UserId) : IEvent;
public sealed record InvoicePaid(decimal Amount) : IEvent;
public sealed record ShipmentDispatched(string TrackingId) : IEvent;
A conventional dispatcher tests the runtime type with a switch. A dynamic dispatcher instead forwards the value to an overloaded method:
#1 Best Overall
public sealed class EventDispatcher
{
public void Dispatch(IEvent evt)
{
ArgumentNullException.ThrowIfNull(evt);
DispatchCore((dynamic)evt);
}
private void DispatchCore(UserRegistered evt)
{
// Handle registration
}
private void DispatchCore(InvoicePaid evt)
{
// Handle payment
}
private void DispatchCore(ShipmentDispatched evt)
{
// Handle shipment
}
private void DispatchCore(IEvent evt)
{
throw new NotSupportedException(evt.GetType().FullName);
}
}
The public entry point accepts IEvent, so ordinary static overload resolution cannot choose one of the concrete handlers. The cast to dynamic tells the compiler to generate a dynamic call site. When Dispatch executes, the binder sees the actual object type and chooses the most specific applicable DispatchCore overload. The interface overload is an explicit fallback for an event that has no dedicated handler.
The sample uses records only to keep the event declarations short. Classes, structs, and types from another assembly work in the same way, provided the runtime binder can access the selected overloads.
What is deferred to runtime
Microsoft describes dynamic as a static type whose value bypasses normal compile-time checking for dynamic operations: “The dynamic type is a static type, but an object of type dynamic bypasses static type checking.” See Microsoft’s explanation of type dynamic.
Only the operation involving a dynamic operand is deferred. The compiler still checks the surrounding method, the declaration of IEvent, the cast itself, and all statically typed code. In the example, the compiler does not validate which DispatchCore overload will be selected for every possible runtime object. The binder performs that member lookup and overload resolution when the call runs.
Receiver versus argument
A dynamic receiver defers member lookup:
dynamic handler = GetHandler();
handler.Process(evt);
A dynamic argument defers overload selection while the receiver and method name remain statically known:
Rank #2
Process((dynamic)evt);
The event-dispatch pattern uses the second form. The method name and containing type are fixed in source; the argument’s runtime type determines the overload.
What the binder considers
At execution time, the binder examines the runtime types of dynamic arguments, finds accessible candidate methods, applies normal C#-style overload rules, and produces a call-site rule. A UserRegistered object selects the UserRegistered overload even though the variable entering the method was typed as IEvent. A derived event with no exact overload can select a compatible base-class or interface overload if one exists. If two candidates are equally good, or no candidate is applicable, the operation fails at runtime instead of producing a compile-time diagnostic.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Runtime failure modes you must design for
Moving dispatch out of a switch also moves several classes of mistakes to the point where the event is processed.
Missing overloads
If the fallback overload is removed and a new event reaches the dispatcher, the dynamic binder cannot find a suitable method and throws a runtime binding exception. Keeping an explicit IEvent fallback lets you produce a deliberate “unhandled event” error, metric, or dead-letter action. It also prevents accidental recursion: do not make the dynamic entry method and the fallback overload share the same signature and name.
Ambiguous overloads
An event implementing multiple unrelated interfaces can make two overloads equally applicable. The compiler cannot warn about every such case because the concrete runtime type may come from a plugin or deserializer. Add a more specific overload, redesign the interfaces, or reject the event explicitly.
Null values
A null dynamic argument has no runtime type. Depending on the overload set, the binder can select a reference-type overload, report ambiguity, or fail. Guard the event before the dynamic call, as the sample does with ArgumentNullException.ThrowIfNull, and treat null as an input-contract violation rather than a dispatch case.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Access and project references
Dynamic binding obeys accessibility and other language restrictions. Microsoft documents these restrictions and runtime binding diagnostics in Resolve errors related to dynamic binding and the dynamic type. The documentation specifically warns that dispatch to a conditional method can fail at runtime; do not rely on a method marked with [Conditional] as a dynamic handler.
Modern .NET 5 and later projects include the required runtime support types. A .NET Framework project may need an explicit reference to Microsoft.CSharp.dll. A missing reference can appear as a build or runtime problem even though the C# syntax is valid.
Runtime-only data problems
Reflection-created objects, proxy types, version-skewed assemblies, and deserialization changes can all alter the runtime type that reaches the call site. Log the concrete type and the event identifier before dispatch. If handlers are security-sensitive, validate the source and permitted type set before invoking dynamic code.
How the DLR executes and caches a dynamic call
The Dynamic Language Runtime (DLR) supplies binders and call sites for dynamic operations. The first time a particular call site sees a combination of runtime types, the binder resolves the operation and creates a rule. Later calls with matching types can reuse that cached rule instead of repeating the full binding process. Microsoft’s DLR overview, last updated March 30, 2024, describes this call-site and rule-caching mechanism.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsRank #4
Caching explains why a dynamic dispatcher may behave differently after warm-up, but it is not a performance result. Cache misses, many distinct runtime types, exception paths, allocations in handlers, logging, queue I/O, and the cost of the work performed by each handler can dominate the dispatch itself. A type-pattern switch may be faster, slower, or indistinguishable for a particular workload. Benchmark representative event mixes on the exact .NET version and build configuration you ship.
A practical benchmark plan
- Implement the dynamic dispatcher and the equivalent switch without changing handler work.
- Use the same preconstructed event objects and the same sequence of event types for both implementations.
- Measure cold calls separately from warmed-up steady-state calls, because the first dynamic binding includes rule creation.
- Test a uniform stream, a mixed stream, and a stream containing many distinct derived types.
- Record throughput, latency distribution, allocations, and the frequency of fallback or exception paths.
- Repeat on the target runtime, processor architecture, and build mode; do not generalize a result from a debug run or a different .NET release.
The type-pattern switch alternative
C# pattern matching can express the same routing explicitly:
public void Dispatch(IEvent evt)
{
ArgumentNullException.ThrowIfNull(evt);
switch (evt)
{
case UserRegistered registered:
Handle(registered);
break;
case InvoicePaid paid:
Handle(paid);
break;
case ShipmentDispatched shipped:
Handle(shipped);
break;
default:
throw new NotSupportedException(evt.GetType().FullName);
}
}
private void Handle(UserRegistered evt) { }
private void Handle(InvoicePaid evt) { }
private void Handle(ShipmentDispatched evt) { }
Patterns match the runtime type and can bind a typed variable for the case body. The syntax, type compatibility, and unreachable-pattern conditions are visible to the compiler and to code reviewers. Microsoft’s C# pattern reference documents type patterns and their use in switch statements and expressions.
A switch statement over an interface is not automatically exhaustive: another assembly can introduce a new implementation. Keep a default branch when unknown events must be rejected or quarantined. A switch expression can produce a compiler warning when it is not exhaustive, but warnings are not a substitute for a runtime policy.
Dynamic dispatch versus a type-pattern switch
| Concern | dynamic overload dispatch |
Type-pattern switch |
|---|---|---|
| Binding time | Applicable members and overloads are selected when the dynamic operation executes. | The dispatch structure and pattern expressions are checked while compiling; the actual type test still occurs at runtime. |
| Failure location | Missing or ambiguous members, restricted operations, and some access problems can surface only during execution. | Many malformed patterns and unreachable cases are diagnosed during compilation; an unknown runtime type reaches the explicit default branch. |
| Closed event set | Requires a fallback or a test to discover that a handler is missing. | Each case is listed together, making the supported set easy to audit. |
| Extending the set | Add an overload; callers do not change, but omissions may be noticed only when the new event runs. | Add a case and handler in the dispatcher; the changed source makes the extension visible. |
| Handler organization | Each event’s behavior can live in an overload, which can reduce a large central branch. | Routing remains centralized, while the case can call separate handlers. |
| Performance evidence | DLR rule caching can reduce repeated binding work, but it does not establish superiority. | Often has a direct, statically structured dispatch path, but speed still depends on the real code and runtime. |
Maintainability and extensibility trade-offs
When overloads improve the design
- The application already models behavior as overloads and the runtime type is the intended source of truth.
- Handlers are independent and adding a new event should mean adding one overload rather than editing a long conditional block.
- The event family is open-ended, supplied by modules that can be deployed independently.
- A common fallback, logging policy, and test suite make unknown types observable.
Even in this design, document the overload contract. A reader should know where the dynamic boundary is, which types are supported, and what happens to an unhandled event.
Best Value
When the switch is clearer
- The event set is deliberately closed and changes are reviewed as part of one application.
- Correctness depends on seeing every route in one file.
- You want compiler diagnostics for pattern mistakes and straightforward step-through debugging.
- Handlers need different guards or conditions that would make a growing overload set difficult to understand.
A switch also makes policy decisions obvious: a default branch can reject, ignore, or quarantine unknown events without relying on binder behavior.
A safe implementation checklist
- Define the boundary. Accept a statically typed interface or base class at the external boundary, and cast only the argument used for dispatch.
- Reject null first. Do not allow a null value to enter overload resolution.
- Provide a deliberate fallback. Use an
IEventoverload or an equivalent guard that records the concrete type and fails predictably. - Keep handlers unambiguous. Review interface combinations and inheritance so a new event cannot match two overloads equally.
- Check accessibility and target framework support. Verify handler visibility and, for .NET Framework, the
Microsoft.CSharp.dllreference. - Test runtime behavior. Cover every supported concrete type, an unknown implementation, null input, derived types, and ambiguous candidates.
- Exercise failure paths in production-like conditions. Confirm that a binding exception is logged with the event type and does not silently drop a message.
- Benchmark before optimizing. Compare warmed and cold paths with identical handler work on the deployment runtime.
Debugging a failed dynamic call
Start with the exact runtime type:
Console.WriteLine(evt.GetType().AssemblyQualifiedName);
Then verify that the expected overload is accessible and that no proxy or derived type changes the applicable candidates. If the exception reports an ambiguous call, inspect every overload accepting a base class or interface. If it reports that no best method exists, add a supported overload or route the type to the fallback intentionally.
For a suspected project-configuration issue, inspect the target framework and references before changing the code. The documented dynamic-binding diagnostics at Microsoft Learn distinguish language restrictions from missing runtime support. Do not “fix” a binding error by adding more overloads until you know which candidate the runtime is trying to select.
Choosing a dispatch strategy
Choose dynamic overload dispatch when runtime overload selection is a deliberate architectural feature, the event family is extensible, and you are prepared to test and observe runtime failures. Keep the type-pattern switch when the set of events is known, the compiler-visible routing is valuable, or a missing case should be obvious during code review.
The choice is about binding time and design communication, not a guaranteed speed improvement. A dynamic call site can cache rules, while a switch keeps its alternatives explicit; only measurements of your handlers, event distribution, and target runtime can establish the practical difference.
Further reading
For a broader C# reference that includes dynamic binding and pattern matching, see the publisher’s listing for C# 12 in a Nutshell. It is general reference material rather than a requirement for implementing either dispatcher.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.

