Switch Expressions
C# 8+ switch expressions replace verbose switch statements with concise, expression-oriented syntax that returns a value.
Introduction
C# 8+ switch expressions replace verbose switch statements with concise, expression-oriented syntax that returns a value. Combined with relational patterns, logical patterns (and/or), and property patterns, they power modern API mapping and domain logic.
Exhaustiveness checking warns when you miss enum cases — critical when adding new payment types or diagnosis codes. Switch expressions appear constantly in ASP.NET Core minimal API result mapping and functional-style service layers.
This lesson contrasts classic switch, switch expressions, and when to reach for if/else instead.
The story
A logistics company quotes shipping rates based on destination zone, package weight, and whether the customer paid for express delivery. A domestic envelope under one kilogram costs a flat fee; heavier domestic parcels add per-kilo surcharges; international shipments use an entirely different formula.
Switch expressions let you express that pricing table as a single expression that returns a decimal — cleaner than a dozen nested if statements and easier to unit test.
Understanding the topic
Key concepts
- Switch expression syntax: value switch { pat => result, _ => default }
- Arms must cover all cases or include discard _.
- Property patterns: obj is { Status: Active, Balance: > 0 }
- Relational patterns: x is > 0 and < 100
- Tuple switch for multi-value dispatch.
- Switch statements still valid for void side-effect branches.
flowchart LRInput[Value] --> Switch{switch expression}Switch --> Arm1[Pattern Arm 1]Switch --> Arm2[Pattern Arm 2]Switch --> Default[_ default]
Step-by-step explanation
- Evaluate switch input expression once.
- Match arms top-to-bottom; first match wins.
- All arms return same type in expression form.
- Compiler enforces enum exhaustiveness when possible.
- when guards add extra boolean conditions to patterns.
- Combine with switch statement for mixed case bodies with break.
Practical code example
Enterprise shipping rate calculator with relational and property patterns:
namespace TechLearningPro.SwitchExpressions;public record Shipment(string Zone, decimal WeightKg, bool IsExpress);public static class ShippingCalculator{public static decimal CalculateRate(Shipment s) => s switch{{ WeightKg: <= 0 } => throw new ArgumentException("Weight must be positive."),{ Zone: "DOMESTIC", WeightKg: <= 1, IsExpress: false } => 5.99m,{ Zone: "DOMESTIC", WeightKg: <= 1, IsExpress: true } => 12.99m,{ Zone: "DOMESTIC", WeightKg: > 1 and <= 10 } => 5.99m + (s.WeightKg - 1) * 1.50m,{ Zone: "INTERNATIONAL" } => 24.99m + s.WeightKg * 4.00m,_ => throw new ArgumentException(quot;Unknown zone: {s.Zone}")};}
Line-by-line code explanation
public record Shipment(string Zone, decimal WeightKg, bool IsExpress)bundles the inputs that determine shipping cost.CalculateRate(Shipment s) => s switchstarts a switch expression on the shipment record.{ WeightKg: <= 0 }is a property pattern that rejects invalid weights first.throw new ArgumentException(...)in a switch arm stops invalid data immediately.{ Zone: "DOMESTIC", WeightKg: <= 1, IsExpress: false }matches lightweight standard domestic packages.=> 5.99mreturns the flat rate for that arm — switch arms produce values directly.{ Zone: "DOMESTIC", WeightKg: > 1 and <= 10 }combines relational patterns withand.5.99m + (s.WeightKg - 1) * 1.50madds a per-kilo surcharge above one kilogram.{ Zone: "INTERNATIONAL" }catches all international shipments regardless of weight tier._ => throw new ArgumentException(...)is the discard arm for unknown zones.
Key takeaway: Property patterns destructure records inline. and combines relational constraints. Throw in discard arm documents invalid zones.
Real-world use
Where you'll use this in production
- HTTP status to ProblemDetails mapping in APIs.
- State machine transitions in order fulfillment.
- Error code to user message localization.
- Pricing tier calculation by customer segment.
Best practices
- Prefer switch expressions for value-returning mappings.
- Keep arms readable — extract complex logic to methods.
- Handle new enum values — compiler helps if not exhaustive.
- Use _ discard for true defaults, not silent swallowing.
- Document thrown exceptions for unmatched inputs.
Common mistakes
- Mixing statement and expression syntax incorrectly.
- Forgetting all enum cases — runtime fall-through bugs.
- Side effects inside switch expressions — hard to test.
- Overly clever patterns harming maintainability.
Advanced interview questions
Q1BeginnerSwitch expression vs switch statement?
Q2BeginnerWhat is the discard pattern _?
Q3IntermediateProperty pattern example?
Q4Intermediatewhen clause purpose?
Q5AdvancedDesign exhaustiveness for payment method enum adding Crypto.
Summary
Switch expressions are concise, return values, and support rich patterns. Property and relational patterns reduce casting boilerplate. Use for mapping discrete inputs to outputs in domain logic. Compiler exhaustiveness catches missing enum cases. Next: loops for iteration and aggregation.