ess logic, providing near-polymorphic performance with superior refactor safety. However, in latency-sensitive hot paths, virtual dispatch remains the performance ceiling. The "WOW" factor is the Refactor Safety metric: switch expressions shift error detection from runtime to compile-time. When a new case is added to a record or enum, the compiler flags every switch expression that lacks coverage, eliminating a entire class of bugs that plague if-else and dictionary-based approaches.
Core Solution
Implementing pattern matching effectively requires a structured approach that aligns the pattern type with the domain semantics.
1. Select the Appropriate Pattern Type
C# offers distinct pattern types. Choosing the wrong one introduces unnecessary complexity.
- Type Patterns: Use for polymorphic checks.
- Property Patterns: Use for inspecting state without casting.
- Positional Patterns: Use exclusively with
record types that define Deconstruct.
- Tuple Patterns: Use for multi-variable state matching.
- Relational Patterns: Use for range checks (C# 9+).
- List Patterns: Use for sequence matching (C# 11+).
2. Implementation: The switch Expression
Replace verbose if-else chains with switch expressions for value transformation. This enforces functional purity by requiring a result for every path.
// Domain Model
public record PaymentRequest(decimal Amount, string Currency, bool IsRecurring);
public enum PaymentStatus { Pending, Processed, Failed, Refunded }
// Implementation
public PaymentStatus ProcessPayment(PaymentRequest request) =>
request switch
{
{ Amount: <= 0 } => throw new ArgumentException("Amount must be positive"),
{ Currency: "USD", IsRecurring: true } => PaymentStatus.Processed,
{ Currency: "EUR", Amount: > 1000 } => PaymentStatus.Pending,
{ Currency: var c } when c.StartsWith("X") => PaymentStatus.Failed,
_ => PaymentStatus.Pending
};
Rationale: The property pattern { Amount: <= 0 } checks state without variable extraction. The when clause handles complex logic that cannot be expressed in the pattern itself. The discard _ ensures exhaustiveness.
3. Recursive Patterns for Hierarchical Data
When dealing with tree structures or nested records, recursive patterns allow decomposition in a single expression.
public record TreeNode(string Value, TreeNode? Left, TreeNode? Right);
public string FormatTree(TreeNode node) => node switch
{
{ Value: var v, Left: null, Right: null } => $"[Leaf: {v}]",
{ Value: var v, Left: { Value: var l }, Right: null } => $"[Node: {v} -> Left: {l}]",
{ Value: var v, Left: var l, Right: var r } => $"[Node: {v} ({FormatTree(l)} | {FormatTree(r)})]",
null => throw new ArgumentNullException(nameof(node))
};
Architecture Decision: Use recursive patterns when the traversal logic is centralized. If traversal behavior varies significantly by node type, consider the Visitor pattern instead.
4. List Patterns for Sequence Validation
C# 11 introduced list patterns, enabling structural matching on arrays and spans. This is critical for parsing protocols or validating input sequences.
public bool IsValidCommand(string[] tokens) => tokens switch
{
["GET", var resource] => true,
["POST", var resource, var payload, ..] => !string.IsNullOrEmpty(payload),
["DELETE", var resource, ..] => true,
_ => false
};
Note: The .. slice pattern matches zero or more elements. This avoids manual index checking and bounds errors.
Pitfall Guide
Production experience reveals consistent failure modes when pattern matching is misused.
-
The Nesting Pyramid:
- Mistake: Creating patterns with nesting depth > 3.
- Impact: Readability collapses. Debugging becomes difficult as the compiler error messages for nested patterns are often opaque.
- Fix: Extract sub-patterns into local functions or helper methods. Keep the
switch expression flat.
-
Ignoring Exhaustiveness Warnings:
- Mistake: Suppressing
CS8509 (The switch expression does not handle all possible inputs) or relying on _ blindly.
- Impact: Adding a new enum value or record property silently breaks logic at runtime.
- Fix: Treat
CS8509 as an error in .editorconfig. Explicitly handle all cases. Use _ only for truly irrelevant inputs.
-
Side Effects in Patterns:
- Mistake: Calling methods with side effects inside
when clauses or pattern guards.
- Impact: Patterns are evaluated lazily and may short-circuit. Side effects become unpredictable and non-deterministic.
- Fix: Patterns must be pure. Perform side effects in the result expression, not the guard.
-
Performance Degradation in Hot Loops:
- Mistake: Using complex property patterns with multiple
when clauses inside tight loops processing millions of items.
- Impact: JIT cannot optimize complex patterns as aggressively as simple type checks or virtual calls.
- Fix: Profile hot paths. If latency is critical, revert to
if-else or virtual dispatch for the hot section. Use patterns for cold paths and configuration logic.
-
Confusing var and Discard:
- Mistake: Using
var x when the value is never used, or using _ when the value is needed.
- Impact:
var x allocates a variable; _ does not. Misuse leads to unnecessary stack allocation or compilation errors.
- Fix: Use
_ for discards. Use var only when you need to reference the matched value in a when clause or the result.
-
Null Handling Ambiguity:
- Mistake: Assuming patterns automatically handle nulls or mixing
is null with property patterns incorrectly.
- Impact:
NullReferenceException if null inputs are not explicitly matched.
- Fix: Always include a
null case or use not null constraint. In nullable reference type contexts, null patterns are essential.
// Correct null handling
string result = input switch
{
null => "Empty",
not null => input.ToString(),
};
-
Overuse for Simple Type Checks:
- Mistake: Using
switch expressions for simple type checks where is or as is sufficient.
- Impact: Unnecessary verbosity.
- Fix: Use
if (obj is Type t) for single checks. Reserve switch for multiple cases or value transformation.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Business Rule Engine | switch Expression | High readability, compiler-enforced exhaustiveness, easy to modify rules. | Low |
| Hot Loop Processing | Virtual Dispatch / Delegates | Minimal overhead, JIT optimizes virtual calls aggressively. | Medium (Refactor) |
| Protocol Parsing | List Patterns / Tuple Patterns | Structural matching on sequences reduces boilerplate index logic. | Low |
| Complex State Machine | Tuple Patterns with when | Matches multi-variable state compactly. | Low |
| Polymorphic Behavior | Virtual Methods / Interface | Behavior belongs to the type; patterns violate encapsulation if overused. | Low |
| Legacy Code Modernization | is Type Patterns | Low risk entry point; improves readability without architectural change. | Low |
Configuration Template
Copy this .editorconfig snippet to enforce pattern matching best practices across the team.
[*.cs]
# Enforce exhaustive switch expressions
dotnet_diagnostic.CS8509.severity = warning
# Enforce exhaustive switch statements
dotnet_diagnostic.CS8524.severity = warning
# Prefer pattern matching over is-type-check + cast
dotnet_style_prefer_pattern_matching = true:suggestion
# Prefer pattern matching over as-type-check + null-check
dotnet_style_prefer_switch_expression = true:suggestion
# Enable nullable reference types (recommended for pattern matching)
nullable = enable
Quick Start Guide
- Update SDK: Ensure your environment supports C# 10 or later for extended property patterns and
switch expressions.
dotnet --list-sdks
- Enable Features: Add language version to your
.csproj if not using implicit defaults.
<PropertyGroup>
<LangVersion>11</LangVersion>
</PropertyGroup>
- Refactor One Class: Identify a class with a complex
if-else chain. Convert it to a switch expression.
// Before
if (status == "Active") return 1;
else if (status == "Pending") return 0;
else return -1;
// After
public int GetPriority(string status) => status switch
{
"Active" => 1,
"Pending" => 0,
_ => -1
};
- Run Analysis: Execute
dotnet build and review warnings. Address CS8509 warnings immediately to verify exhaustiveness.
- Add Benchmarks: If the refactored code is in a performance-sensitive area, add a BenchmarkDotNet test to verify no regression.
Pattern matching in C# is a mature, high-performance feature that, when applied with architectural discipline, significantly elevates code safety and maintainability. Treat it as a semantic tool, not just syntactic sugar, and your codebase will benefit from reduced complexity and stronger compile-time guarantees.