Switch Statement and Switch Expressions — Modern Pattern Matching

When you need to branch on multiple possible values of a single expression, a switch is cleaner than a chain of if/else if statements. C# has two forms: the classic switch statement (similar to other languages) and the modern C# 8 switch expression which is more concise and enforces exhaustiveness. Switch expressions are widely used in ASP.NET Core source code — for mapping HTTP status codes, handling command types, and routing requests by type.

Classic Switch Statement

string dayName = "Monday";

switch (dayName)
{
    case "Saturday":
    case "Sunday":
        Console.WriteLine("Weekend");
        break;              // required — prevents fall-through to next case
    case "Monday":
    case "Friday":
        Console.WriteLine("Almost there");
        break;
    default:                // executed when no case matches
        Console.WriteLine("Weekday");
        break;
}
// Output: Almost there

// Switch on an integer
int statusCode = 404;
switch (statusCode)
{
    case 200:
        Console.WriteLine("OK");           break;
    case 201:
        Console.WriteLine("Created");      break;
    case 400:
        Console.WriteLine("Bad Request");  break;
    case 401:
    case 403:
        Console.WriteLine("Auth error");   break;
    case 404:
        Console.WriteLine("Not Found");    break;
    default:
        Console.WriteLine("Unknown");      break;
}
Note: Unlike C and Java, C# does not allow implicit fall-through between cases — you must always end each case with break, return, throw, or goto case. This prevents one of the most common bugs in C-family languages where a forgotten break accidentally executes the next case. The compiler enforces this at compile time, not runtime. The only exception is empty cases that share a body (like case "Saturday": case "Sunday": stacked with no code between them).
Tip: For switching on string values in C# 8+, prefer the switch expression (shown next) over the classic statement. Switch expressions are more concise, evaluated as a single expression (usable directly in assignments), and the compiler warns you if you miss a possible value when you use enums. They are increasingly the idiomatic choice in modern .NET code and appear throughout ASP.NET Core minimal API route handlers.
Warning: Switch statements in C# are case-sensitive for string values. case "Monday" does not match "monday" or "MONDAY". If you are switching on user input or data from external sources, normalise the case first: switch (input.ToLower()) or switch (input.Trim().ToLowerInvariant()). In API controllers, model binding typically normalises input before it reaches your switch logic, but be explicit when processing raw strings.

Switch Expression (C# 8+)

// Switch expression — evaluates to a value
int statusCode2 = 404;

string message = statusCode2 switch
{
    200 => "OK",
    201 => "Created",
    400 => "Bad Request",
    401 or 403 => "Auth Error",    // 'or' pattern — matches either value
    404 => "Not Found",
    >= 500 => "Server Error",      // relational pattern
    _ => "Unknown"                 // _ is the discard / default arm
};

Console.WriteLine(message);   // "Not Found"

// Switch expression on enum — compiler warns on missing members
enum Direction { North, South, East, West }
Direction dir = Direction.North;

string arrow = dir switch
{
    Direction.North => "↑",
    Direction.South => "↓",
    Direction.East  => "→",
    Direction.West  => "←",
    // No _ needed — all enum values covered (compiler verified)
};

// Switch expression with when guard clause
int score = 73;
string grade = score switch
{
    >= 90                  => "A",
    >= 80                  => "B",
    >= 70 and < 80         => "C",        // 'and' combines patterns
    >= 60                  => "D",
    _ when score < 0       => "Invalid",  // when guard adds extra condition
    _                      => "F"
};
Console.WriteLine(grade);   // "C"

// Type pattern — switch on the runtime type of an object
object shape = new Circle(radius: 5.0);

double area = shape switch
{
    Circle c    => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.Width * r.Height,
    _           => throw new ArgumentException("Unknown shape")
};

record Circle(double Radius);
record Rectangle(double Width, double Height);

Common Mistakes

Mistake 1 — Forgetting break in a classic switch (compile error)

❌ Wrong — compile error “Control cannot fall through from one case label to another”:

switch (x)
{
    case 1:
        DoA();
        // missing break! compile error in C#
    case 2:
        DoB();
        break;
}

✅ Correct — always end each case with break, return, or throw.

Mistake 2 — Non-exhaustive switch expression (runtime exception)

❌ Wrong — throws SwitchExpressionException if none of the arms match:

string result = statusCode switch { 200 => "OK", 404 => "Not Found" };
// If statusCode is 500: SwitchExpressionException at runtime!

✅ Correct — always include _ => defaultValue or _ => throw new ... as the final arm.

🧠 Test Yourself

What is the key difference between a switch statement and a switch expression in C#?