Skip to main content

Command Palette

Search for a command to run...

Strategy Pattern vs Switch Expressions vs Dictionary Dispatch in .NET: Which Should Your Team Use?

Updated
โ€ข13 min read
Strategy Pattern vs Switch Expressions vs Dictionary Dispatch in .NET: Which Should Your Team Use?

Every .NET team eventually reaches the same crossroads: a conditional block that dispatches behaviour based on a type, value, or enum โ€” and the growing suspicion that the current approach won't scale. The Strategy Pattern, C# switch expressions, and dictionary dispatch all solve this problem, but each carries different costs in readability, testability, extensibility, and runtime performance. Choosing the wrong one early means refactoring it away later, often at the worst possible moment. Understanding where each approach belongs โ€” and where it does not โ€” is the kind of judgment that separates maintainable enterprise codebases from ones that quietly accumulate technical debt.

If you want to see these patterns in practice with production-ready code and edge cases covered, the full implementations are available on Patreon โ€” ready to run and adapt to your real codebase.

Understanding how this decision fits into your service registration strategy is equally important. The Keyed Services vs Factory Pattern vs Named Services in ASP.NET Core guide covers the DI side of this decision โ€” worth reading alongside this article when the dispatch logic lives behind an interface.

What Problem Are We Actually Solving?

All three approaches address the same core challenge: selecting and executing the right behaviour at runtime based on a discriminant โ€” a status, a payment method, an event type, a document format. The difference lies in how that selection is encoded, who controls it, and how easy it is to change later.

The naive starting point is an if-else chain or a switch statement. It works until it doesn't โ€” typically when new cases appear, when the logic grows complex enough to demand unit testing at the individual branch level, or when two different parts of the codebase need to resolve the same discriminant independently.

Option 1: The Strategy Pattern with Dependency Injection

The Strategy Pattern encodes each branch as a separate class implementing a shared interface. ASP.NET Core's DI container registers all implementations, and a resolver โ€” either a factory, a keyed service lookup, or an IEnumerable<T> filter โ€” selects the right one at runtime.

The key advantage is that each strategy is independently testable, independently deployable in unit tests, and fully decoupled from the resolution logic. Adding a new case means adding a new class and registration, not touching existing code. This is the Open/Closed Principle in its most literal form.

The cost is ceremony. Three strategies mean three classes, three registrations, and a resolution mechanism that itself needs to be correct. For small, stable discriminant sets, this overhead is real.

When to reach for the Strategy Pattern:

  • The number of variants is expected to grow (new payment providers, new notification channels, new document processors)
  • Each variant has non-trivial logic that needs its own tests
  • The strategy implementations have their own dependencies that should be injected
  • The resolution happens across multiple call sites or services
  • Extensibility via plugin or module registration is a future requirement

When to avoid it:

  • The discriminant set is fixed and small (2-3 cases that will never expand)
  • The logic per branch is a single line or trivially simple
  • The extra classes add navigation burden without adding testability benefit

Option 2: Switch Expressions (C# 8+)

C# switch expressions, introduced in C# 8 and extended significantly through C# 13 and 14, bring pattern matching directly into the language. They support type patterns, property patterns, positional patterns, and relational patterns โ€” making them far more powerful than a traditional switch statement.

A switch expression is the right tool when the dispatch logic is local, the result is a value (not a side effect), and the discriminant set is closed โ€” meaning you control all the cases and new ones don't arrive at runtime.

The compiler provides exhaustiveness checking for enums and discriminated types, which means the switch expression fails at compile time if a case is missed โ€” something neither dictionary dispatch nor the Strategy Pattern provides without extra effort.

When to reach for switch expressions:

  • Mapping from one value to another (enum โ†’ string, status โ†’ HTTP code, tier โ†’ discount rate)
  • The discriminant is a closed set under your control (an enum, a sealed type hierarchy)
  • The branch logic is a simple expression, not a multi-step operation
  • The dispatch is local to one method and not shared across services
  • You want compile-time exhaustiveness guarantees

When to avoid them:

  • Branches contain complex, multi-step logic that deserves its own test surface
  • The discriminant set is open-ended or runtime-provided (plugins, user-configurable processors)
  • The logic involves dependencies โ€” injected services, async I/O, external calls

Option 3: Dictionary Dispatch

Dictionary dispatch maps a key to a Func<T>, Action, or pre-constructed handler object stored in a Dictionary<TKey, TValue>. It offers O(1) lookup by design and is the natural fit when the key space is large, when the mapping is built at runtime from dynamic configuration, or when the cost of switch compilation overhead (on very large switch blocks) matters.

In practice, dictionary dispatch is most useful when the mapping comes from data โ€” from a configuration file, a database, or a set of registered plugins โ€” rather than from compile-time case labels. It is also useful when multiple services each need to resolve their own subset of the same key space independently.

The downside is that dictionaries give up exhaustiveness checking entirely. Missing keys are runtime failures. The structure is harder to reason about when reading the codebase, because the reader must trace both the dictionary construction and the invocation site to understand the full dispatch graph.

When to reach for dictionary dispatch:

  • The key-to-handler mapping is built from runtime data (configuration, database, plugin registration)
  • The key space is large (dozens or more cases) and O(1) lookup is preferable to a long switch
  • Multiple registration sources need to contribute to the same dispatch map
  • The handlers are lightweight functions (delegates, lambdas) without DI dependencies

When to avoid it:

  • The mapping is static and known at compile time (a switch expression is simpler and safer)
  • The handlers have injected dependencies (use the Strategy Pattern instead)
  • You need exhaustiveness guarantees (dictionary dispatch has none)

Side-by-Side Comparison

Dimension Strategy Pattern Switch Expression Dictionary Dispatch
Extensibility โœ… Open/Closed by design โŒ Requires code change โš ๏ธ Dynamic if built at runtime
Exhaustiveness check โŒ Runtime failure on missing key โœ… Compiler-enforced (enums/sealed) โŒ Runtime failure on missing key
Testability โœ… Each strategy independently testable โš ๏ธ Tested as part of the calling method โš ๏ธ Mapping logic needs integration
DI support โœ… Native โ€” inject into strategies โŒ No DI in branch logic โš ๏ธ Possible but awkward
Ceremony โŒ High โ€” multiple classes โœ… Low โ€” inline expression โš ๏ธ Medium โ€” constructor or setup method
Lookup performance โš ๏ธ Depends on resolution โœ… Compiler-optimised jump table โœ… O(1) hash lookup
Runtime flexibility โš ๏ธ New registration required โŒ Compile-time only โœ… Fully dynamic
Best for Open variant sets with logic Closed value mappings Dynamic, data-driven routing

Real-World Trade-Offs

The Notification Routing Problem

Consider an ASP.NET Core API that dispatches notifications across email, SMS, push, and webhook channels. New channels will be added as the product grows. Each channel has configuration, retry logic, and templating concerns.

This is the Strategy Pattern problem. Each channel is a strategy, each strategy has dependencies, and the open-ended growth of the channel list is precisely the extensibility pressure the pattern is designed to absorb. Switch expressions and dictionary dispatch both require code changes for every new channel โ€” which means the team that added the new channel has to understand the dispatch site, not just the new handler.

The HTTP Status Code Mapping Problem

Consider mapping a domain exception type to the appropriate HTTP status code in a global exception handler. The set of exception types is finite, controlled by the team, and the mapping is a pure value expression: NotFoundException โ†’ 404, ConflictException โ†’ 409, ValidationException โ†’ 422. This is a switch expression problem. Bringing in the Strategy Pattern for this adds four files of ceremony for what should be four lines of code.

The Payment Method Routing Problem with Runtime Plugins

Consider a payment processing platform where payment method handlers are registered as plugins from a database table. The handler for each payment method is resolved by a string key that comes from the database at startup. This is a dictionary dispatch problem โ€” the mapping is built at runtime, the keys come from data, and the handlers are lightweight delegates that call into a pre-resolved service. The Strategy Pattern is still viable here but requires a more complex keyed-service setup; dictionary dispatch is simpler when the plugins are data-driven rather than code-driven.

What About Combining Them?

In practice, production codebases often combine all three. A common pattern in ASP.NET Core looks like this:

  • Switch expression at the top of the pipeline to route between two or three broad categories
  • Strategy Pattern within each category where the variants are open-ended and DI-managed
  • Dictionary dispatch in specific hot paths where lookup performance matters and the keys come from runtime data

The mistake to avoid is defaulting to one approach regardless of context. Teams that over-apply the Strategy Pattern end up with an explosion of single-method classes. Teams that over-apply switch expressions end up with unmaintainable blocks that nobody wants to touch. Teams that over-apply dictionary dispatch end up with opaque mapping tables and silent runtime failures.

How This Connects to Common DI Mistakes

The Strategy Pattern's biggest failure mode in ASP.NET Core codebases is getting the service lifetime wrong โ€” registering strategies as singletons when they depend on scoped services, or resolving them through a root provider and triggering the captured dependency problem. If your Strategy Pattern resolution involves any service lookup, the guidance in 7 Common ASP.NET Core Dependency Injection Mistakes (And How to Fix Them) directly applies โ€” particularly the sections on captive dependencies and lifetime mismatches.

Making the Right Call for Your Team

Three questions guide the decision:

Is the variant set closed or open? If it is closed (you control all the cases and they are known at compile time), switch expressions give you compiler safety for free. If it is open, the Strategy Pattern earns its ceremony.

Does each variant have its own dependencies or complex logic? If yes, the Strategy Pattern is the right level of abstraction. If no, the overhead is not worth it.

Is the mapping data-driven or code-driven? If the key-to-handler mapping comes from runtime data, dictionary dispatch is the natural fit. If it comes from source code, switch expressions or the Strategy Pattern will be safer.

The best teams treat these as tools in a toolbox โ€” not mutually exclusive philosophies. Applying the right tool to the right problem is what keeps .NET codebases readable and changeable over time.


โ˜• Prefer a one-time tip? Buy us a coffee โ€” every bit helps keep the content coming!


FAQ

What Is the Difference Between the Strategy Pattern and a Switch Expression in C#?

The Strategy Pattern encodes each behaviour variant as a separate class implementing a shared interface, with variant selection handled by a resolver or DI container at runtime. A switch expression is a language construct that selects a value or invokes logic inline based on a pattern match, with all cases defined at compile time. The Strategy Pattern is better for open-ended, dependency-carrying variants; switch expressions are better for closed value mappings where compile-time exhaustiveness checking is valuable.

When Should I Prefer a Switch Expression over the Strategy Pattern in ASP.NET Core?

Use a switch expression when the dispatch is mapping from one value to another (for example, status to HTTP code or enum to string), when the set of cases is fixed and compiler-controlled, and when the branch logic is a simple expression rather than a multi-step operation with dependencies. The Strategy Pattern adds a level of indirection that is only justified when the variants are independently testable, have injected dependencies, or are expected to grow over time.

Is Dictionary Dispatch Faster Than a Switch Expression in .NET?

For small to medium discriminant sets (up to roughly 6-7 cases), the C# compiler generates optimised jump tables or binary search trees for switch expressions that are typically faster than dictionary hash lookups. For larger sets (dozens or hundreds of keys), dictionary dispatch at O(1) can outperform the switch. In practice, the performance difference is rarely the deciding factor โ€” correctness, testability, and maintainability should drive the choice first.

Can I Combine the Strategy Pattern with Dictionary Dispatch in ASP.NET Core?

Yes, and it is a common production pattern. The Strategy Pattern defines the interface and implementations; dictionary dispatch (or keyed services) provides the resolution mechanism that maps from a runtime key to the correct strategy instance. This combination gives you the extensibility and testability of the Strategy Pattern with the O(1) runtime lookup of dictionary dispatch. ASP.NET Core's keyed DI services (AddKeyedSingleton, AddKeyedScoped) are the idiomatic way to achieve this since .NET 8.

What Are the Risks of Using Dictionary Dispatch for Behaviour Routing in .NET?

The primary risk is silent runtime failure: if a key is missing from the dictionary, the code throws a KeyNotFoundException at runtime rather than failing at compile time. Unlike a switch expression with an exhaustive pattern match or an enum, the compiler cannot verify that all expected keys are handled. Dictionary dispatch also makes the routing logic harder to trace during a code review or debugging session, because the reader must follow both the dictionary construction and the call site to understand the full dispatch graph. Always include a fallback or explicit null-check for missing keys.

How Does the Strategy Pattern Interact with Scoped Service Lifetimes in ASP.NET Core?

Strategy implementations should be registered with the same lifetime as their most restrictive dependency. If a strategy depends on a scoped service (such as a DbContext), the strategy itself must be scoped โ€” not singleton. Resolving scoped strategies through a DI-registered factory is straightforward; the risk appears when teams store strategy instances in a singleton resolver without accounting for lifetime. This is covered in detail in the dependency injection mistake guides, and it is one of the most common production issues with Strategy Pattern implementations in ASP.NET Core services.

Should I Use the Strategy Pattern for Simple Two-Case Dispatch in .NET?

Generally, no. For two fixed cases โ€” or even three โ€” the ceremony of the Strategy Pattern (interface, two implementation classes, registration, resolver) is not justified unless the variants are expected to grow or each variant has non-trivial logic requiring independent testing. A switch expression or a simple conditional is more readable and easier to maintain for a small, stable discriminant set. Reserve the Strategy Pattern for situations where extensibility and independent testability are genuine requirements, not theoretical ones.

More from this blog

C

Coding Droplets

231 posts

Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.

From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.