Skip to main content
SEMastery
.NET Corebeginner

How to Write Elegant Code With C# Switch Expressions

Learn C# switch expressions the easy way. Master pattern matching with type, relational, property, and list patterns using simple examples, diagrams, and tables.

12 min readUpdated January 23, 2026

Choosing the right counter at the railway station

Picture a busy railway station near your home. You walk in with a question. Maybe you want a ticket. Maybe you want to ask about a late train. Maybe you lost your bag.

There is a helpful guard at the entrance. You tell the guard your need, and the guard points you to the correct counter in one sentence. "Ticket? Counter 3." "Lost bag? Counter 7." "Late train? Ask the help desk."

The guard does not write a long essay. The guard looks at your situation, matches it to the right answer, and sends you off. One input, one clear answer.

A C# switch expression works just like that helpful guard. You give it one value. It looks at that value, matches it against a list of patterns, and hands you back one answer. Short, clean, and easy to read.

In this guide you will learn how switch expressions work, how they differ from the old switch statement, and how pattern matching makes them powerful. We will use simple examples you can try right away.

The station guard turns one question into one clear answer, just like a switch expression.

The old way: the switch statement

Before switch expressions, C# only had the switch statement. It works, but it is wordy. Let us look at a small example. We want to turn a day number into a day name.

string GetDayName(int day)
{
    switch (day)
    {
        case 1:
            return "Monday";
        case 2:
            return "Tuesday";
        case 3:
            return "Wednesday";
        default:
            return "Unknown";
    }
}

Look at all the noise. The word case repeats. The word return repeats. There are colons and a default. For such a simple job, that is a lot of typing.

The switch statement is a set of instructions. It does things. It does not naturally hand back a value. You have to add return or set a variable inside each case. That is where switch expressions help.

The new way: the switch expression

A switch expression says one thing: "Look at this value and give me back the matching result." Here is the same code as a switch expression.

string GetDayName(int day) => day switch
{
    1 => "Monday",
    2 => "Tuesday",
    3 => "Wednesday",
    _ => "Unknown"
};

Notice the differences:

  • The value comes first, then the word switch.
  • Each arm uses the arrow => instead of case and break.
  • The underscore _ is the catch-all, like default.
  • The whole thing is one expression, so we assign it straight to the method.

It is shorter, calmer, and easier to read. Each line reads almost like English: "1 gives Monday."

How a switch expression runs

Input
Match arm
Return value

Steps

1

Input

You pass one value in

2

Check arms

Top to bottom, find first match

3

Guard check

If a when clause exists, test it

4

Return value

Hand back the matching result

The input is checked against each arm from top to bottom until one matches.

The parts of a switch expression

Let us name the pieces so the rest of the guide is clear.

PartWhat it looks likeWhat it means
InputdayThe value you are testing
KeywordswitchStarts the expression
Arm1 => "Monday"One pattern and its result
Pattern1The thing to match against
Arrow=>Separates pattern from result
Discard_ => ...Catches everything else

A switch expression is just a list of arms wrapped in braces. Each arm is a pattern, an arrow, and a result. The first arm whose pattern matches wins.

Pattern matching: the real magic

The arms above used plain numbers like 1 and 2. Those are constant patterns. But switch expressions can match much smarter patterns. This is called pattern matching, and it is what makes switch expressions feel elegant.

Here are the main pattern types you will use.

PatternExampleUse it to
Constant1, "Mon", trueMatch an exact value
Typeint n, string sMatch the type of an object
Relational> 90, < 0Compare with a number
Property{ Age: > 18 }Look inside an object
List[1, 2, 3]Match the shape of a sequence
Logical> 0 and < 10Join patterns with and, or, not
Varvar xGrab the value into a name
Discard_Match anything left over

Let us see each one in friendly examples.

Relational patterns: comparing numbers

A relational pattern compares the input against a constant using <, >, <=, or >=. This is perfect for grading marks.

char GetGrade(int marks) => marks switch
{
    >= 90 => 'A',
    >= 75 => 'B',
    >= 60 => 'C',
    >= 40 => 'D',
    _     => 'F'
};

Read it from top to bottom. If marks are 95, the first arm >= 90 matches and you get 'A'. If marks are 70, the first two arms fail, but >= 60 matches and you get 'C'.

Order matters here, just like with a list of if-else checks. The first matching arm wins, so you put the most specific or highest checks first.

Logical patterns: joining conditions

You can combine patterns with the words and, or, and not. These are logical patterns. They let you write clear rules without nested if statements.

string DescribeNumber(int n) => n switch
{
    < 0          => "negative",
    0            => "zero",
    > 0 and < 10 => "small",
    >= 10        => "big",
    _            => "unknown"
};

The arm > 0 and < 10 matches numbers between 1 and 9. This reads almost like a sentence. Compare that with a tangle of if (n > 0 && n < 10) blocks, and you can feel how much calmer the switch version is.

A relational and logical pattern routes a number into the right bucket.

Type patterns: matching what an object is

Sometimes you have an object and you want to act based on its actual type. A type pattern does this and even hands you a typed variable to use.

string Describe(object item) => item switch
{
    int n    => $"An int with value {n}",
    string s => $"A string of length {s.Length}",
    bool b   => $"A bool that is {b}",
    null     => "Nothing at all",
    _        => "Some other thing"
};

When item is a string, the arm string s matches, and inside that arm s is already a real string. No casting needed. This is safer than the old way of casting by hand, because the compiler does the work for you. Notice we can even match null as its own arm.

Property patterns: looking inside an object

A property pattern peeks inside an object and checks its fields or properties. Imagine a simple Person record.

record Person(string Name, int Age);
 
string Ticket(Person p) => p switch
{
    { Age: < 5 }            => "Free",
    { Age: >= 5 and < 18 }  => "Child fare",
    { Age: >= 60 }          => "Senior discount",
    _                       => "Adult fare"
};

The pattern { Age: < 5 } means "a Person whose Age is less than 5." You can read inside the object without writing p.Age again and again. You can even nest deeper, like { Address: { City: "Delhi" } }, to look two levels in.

Picking a ticket price by age

Person in
Check Age
Fare out

Steps

1

Person in

A Person object arrives

2

Read Age

Pattern looks at the Age field

3

Match band

Find the first age band that fits

4

Fare out

Return the matching fare

The property pattern checks the Age field inside the Person and routes to a fare.

List patterns: matching the shape of a sequence

List patterns arrived in C# 11. They let you match the shape of an array or list. This is handy for parsing.

string Classify(int[] numbers) => numbers switch
{
    []           => "Empty list",
    [var single] => $"One item: {single}",
    [var a, var b] => $"Two items: {a} and {b}",
    [1, ..]      => "Starts with 1",
    [.., 9]      => "Ends with 9",
    _            => "Something else"
};

Here [] matches an empty array. [var single] matches an array with exactly one item and names it single. The .. is a slice pattern that means "any number of items here." So [1, ..] means "starts with 1, then anything."

Case guards with when

Sometimes a pattern is not enough and you need an extra check. The when keyword adds a condition, called a case guard.

string Shipping(Order order) => order switch
{
    { Total: > 1000 } when order.IsMember => "Free express",
    { Total: > 1000 }                     => "Free standard",
    _                                     => "Paid shipping"
};

The first arm only matches when the total is over 1000 and the customer is a member. The guard runs after the pattern matches.

Safety: handle every case

A big reason switch expressions feel elegant is safety. The compiler studies your arms and warns you if they do not cover every possible input. This catches bugs before users ever see them.

If you skip the discard _ and no arm matches at runtime, the switch expression throws a SwitchExpressionException. So adding a final _ => ... arm is a friendly habit. It says "for everything else, do this."

The compiler checks your arms and warns when a case is missing.

This idea has a fancy name: making invalid states unrepresentable. In plain words, you shape your code so wrong situations simply cannot slip through quietly. Type patterns mixed with property and relational patterns help you remove manual casting and scattered null checks, which makes your code less likely to break.

When to reach for a switch expression

Switch expressions are lovely, but they are not the answer for everything. Here is a simple guide.

SituationGood fit?Why
Turn one input into one valueYesThis is exactly what they do
Map an enum to text or settingsYesClear, short, and exhaustive
Grade marks or band a numberYesRelational patterns shine here
Run many side effects per caseNoA switch statement is clearer
Loop, log, then continue workNoStatements handle flow better

A simple rule: if each branch produces a value, use a switch expression. If each branch does a sequence of actions, a switch statement may read better.

A quick word on the C# version

Switch expressions came in C# 8.0. Relational, logical, and property patterns grew through C# 9 and C# 10. List and slice patterns landed in C# 11. After that, C# 12, C# 13, and C# 14 added no new pattern features, so what you learn here is stable and current. With .NET 10 as the long-term support release and C# 14 shipped, switch expressions are a safe everyday tool.

Looking ahead, C# 15 in the .NET 11 preview is adding union types, which pair beautifully with switch expressions for matching one of several known shapes. But you do not need that to start writing elegant code today.

Putting it together: a tiny calculator

Let us finish with one example that uses several patterns at once. A small calculator that takes two numbers and an operator.

double Calculate(double a, double b, string op) => op switch
{
    "+"               => a + b,
    "-"               => a - b,
    "*"               => a * b,
    "/" when b != 0   => a / b,
    "/"               => throw new DivideByZeroException(),
    _                 => throw new ArgumentException($"Unknown op: {op}")
};

Read it slowly. Each operator maps to one result. The / case uses a guard so we never divide by zero. The discard arm protects us from any operator we did not plan for. In a handful of lines, the whole calculator is clear, safe, and easy to extend.

Quick recap

  • A switch expression takes one input and returns one value, like a station guard pointing you to the right counter.
  • It is shorter than a switch statement: the value comes first, arms use =>, and _ is the catch-all.
  • Pattern matching powers it. You can match by constant, type, relational, property, and list patterns.
  • Join patterns with and, or, and not to write clear rules without nested if blocks.
  • Use when to add an extra case guard on top of a pattern.
  • Always cover every case. The compiler warns you, and a missing case throws a SwitchExpressionException at runtime.
  • Use switch expressions when each branch produces a value. Use a switch statement when each branch does many actions.
  • The features are stable through C# 14 and .NET 10, so you can rely on them every day.

References and further reading

Related Posts