ction-ready code structures.
1. Architecture: Domain Modeling with Records
Pattern matching reaches its full potential when paired with immutable data types. Use record or record struct to define domain entities. Records provide value-based equality and deconstruction support, which integrates seamlessly with property and recursive patterns.
// Define domain as discriminated unions via inheritance and records
public abstract record Command;
public record CreateOrder(string ProductId, int Quantity) : Command;
public record CancelOrder(string OrderId, string Reason) : Command;
public record RefundOrder(string OrderId, decimal Amount) : Command;
2. Switch Expressions and Exhaustive Matching
Replace switch statements with switch expressions. Switch expressions are expressions, meaning they must return a value and must be exhaustive. This forces the developer to handle every case, catching missing logic at compile time.
public decimal CalculateProcessingFee(Command command) => command switch
{
CreateOrder { Quantity: > 10 } => 15.00m,
CreateOrder => 5.00m,
CancelOrder { Reason: "Fraud" } => 0.00m,
CancelOrder => 2.50m,
RefundOrder { Amount: > 1000 } => 10.00m,
RefundOrder => 5.00m,
_ => throw new InvalidOperationException("Unhandled command type")
};
3. Logical Patterns (C# 9+)
Use and, or, and not patterns to combine conditions without nesting. This flattens complex logic and improves readability.
public bool IsValidDiscount(User user) => user is
{ Age: >= 18 and <= 65, Status: not UserStatus.Banned }
or
{ IsPremiumMember: true };
4. List Patterns (C# 11)
List patterns allow matching against sequences, enabling validation of array or span structures directly in the pattern. This is critical for parsing protocols or validating input arrays.
public string ParseResponse(Span<byte> payload) => payload switch
{
[0x01, var status, ..] when status == 0x00 => "Success",
[0x01, var status, ..] => $"Error: {status}",
[0x02, var code, var message, .. var rest] => $"Data: {code} {message}",
_ => "Unknown Protocol"
};
5. Relational and Property Patterns
Combine relational patterns with property patterns to filter data deeply without intermediate variables.
public string GetRiskLevel(Transaction tx) => tx switch
{
{ Amount: > 10000, Country: "XX" } => "Critical",
{ Amount: > 5000, Country: not "US" } => "High",
{ Amount: > 1000 } => "Medium",
_ => "Low"
};
Pitfall Guide
Production experience reveals specific anti-patterns that degrade performance and maintainability. Avoid these common mistakes.
-
Using switch Statements Instead of Expressions:
- Mistake: Using
switch statements for control flow that returns values.
- Impact: Increases verbosity, allows fall-through errors, and reduces compiler enforcement of exhaustiveness.
- Fix: Always prefer
switch expressions when mapping input to output.
-
Side Effects in when Clauses:
- Mistake: Calling methods with side effects inside
when guards.
- Impact:
when clauses may be evaluated multiple times or reordered by the compiler. Side effects lead to unpredictable behavior and race conditions.
- Fix:
when clauses must be pure functions. Extract side effects before the pattern match.
-
Over-Nesting Recursive Patterns:
- Mistake: Creating deeply nested recursive patterns that span multiple lines and levels.
- Impact: Reduces readability and makes debugging difficult. The compiler error messages become obscure.
- Fix: Flatten patterns where possible. Extract complex sub-matches into helper methods or use logical patterns.
-
Pattern Matching on Mutable State:
- Mistake: Matching against objects that change state during the evaluation of the pattern.
- Impact: If a property changes between the check and the binding, the code may operate on inconsistent state.
- Fix: Use pattern matching on immutable snapshots or records. If matching mutable objects, ensure thread safety or capture values immediately.
-
Ignoring the Discard _ in Exhaustive Checks:
- Mistake: Omitting the discard case in a switch expression or using it to mask missing logic.
- Impact: In switch expressions, omitting
_ causes a compiler error if not all cases are covered (good). However, using _ to swallow unhandled cases in business logic hides bugs.
- Fix: Use
_ only when the default behavior is intentional. During development, throw NotImplementedException in the default case to ensure new types are handled.
-
Performance Anti-Pattern: Allocation in Patterns:
- Mistake: Using patterns that trigger allocations, such as matching against
IEnumerable without GetEnumerator optimization or using list patterns on large collections repeatedly.
- Impact: List patterns on arrays are efficient, but on
IEnumerable, they may enumerate multiple times.
- Fix: Use
Span<T> or arrays for list patterns in hot paths. Avoid enumerables in performance-critical matching.
-
Redundant Type Checks:
- Mistake: Combining
is checks with pattern matching unnecessarily.
- Impact:
if (obj is DerivedType dt && dt.Property > 0) is redundant if dt is only used in the condition.
- Fix: Use property patterns directly:
if (obj is DerivedType { Property: > 0 }).
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Simple Type Check | is pattern | Readability and null safety. | Low |
| Complex Dispatch Logic | Switch Expression | Enforces exhaustiveness; concise. | Low |
| Sequence Validation | List Pattern | Expressive; handles variable lengths. | Low |
| High-Throughput Loop | Manual Check / Struct | Avoids pattern overhead; JIT optimizes structs. | High (if pattern causes alloc) |
| Domain Modeling | Records + Patterns | ADT simulation; compile-time safety. | Medium |
| Legacy Code Migration | Incremental is replacement | Low risk; immediate safety gains. | Low |
Configuration Template
Add this .editorconfig snippet to enforce pattern matching styles and enable relevant analyzers in your project.
# Enforce switch expression usage
dotnet_style_prefer_switch_expression = true:suggestion
# Prefer pattern matching over `as` checks
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
# Enable exhaustive switch warnings
dotnet_analyzer_diagnostic.severity = warning
# C# Language Version
is_global = true
dotnet_build_property_langversion = 12
Quick Start Guide
- Upgrade Language Version: Ensure your
.csproj targets C# 12 or higher.
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12</LangVersion>
</PropertyGroup>
- Refactor a Candidate: Identify a method with nested
if checks or a switch statement. Convert it to a switch expression using property and logical patterns.
- Validate Exhaustiveness: Remove the default
_ case temporarily. Let the compiler highlight missing cases. Add handling for each case.
- Run Diagnostics: Execute
dotnet build to verify analyzer warnings. Address any suggestions regarding pattern usage.
- Benchmark: If the refactored code is in a hot path, run a benchmark comparing the old and new implementation to verify performance characteristics.
This guide provides the technical foundation for leveraging C# pattern matching in production environments. By adhering to these patterns and avoiding identified pitfalls, engineering teams can achieve significant gains in code safety, performance, and maintainability.