ZetCode

C# switch expression

last modified November 6, 2026

In this article we show how to use switch expressions in C#.

Switch expressions are a concise enhancement of the classic switch statements, introduced in C# 8. A switch expression consists of arms, each of which consists of a pattern, an optional when guard, the => token, and an expression. The value of the first arm whose pattern matches becomes the value of the whole switch expression.

The arms may evaluate the following:

The arms are evaluated from top to bottom. The discard pattern _ matches any value; it is typically used as the last arm.

C# switch expression value pattern

With a value pattern, also called a constant pattern, the switch arms are based on constant values such as integers or strings.

Program.cs
string[] domains = ["us", "DE", " sk ", "hu", "jp"];

foreach (var domain in domains)
{
    Console.WriteLine($"{domain.Trim().ToLower()} -> {GetCountry(domain)}");
}

string GetCountry(string domain) => domain.Trim().ToLower() switch
{
    "us" => "United States",
    "de" => "Germany",
    "sk" => "Slovakia",
    "hu" => "Hungary",
    _ => "Unknown"
};

In the example, we use a switch expression to map a domain name to its country name.

string GetCountry(string domain) => domain.Trim().ToLower() switch

The value to be matched comes before the switch keyword. The switch expression is often the body of an expression-bodied function.

    _ => "Unknown"

The discard arm is used for all domains that are not listed.

$ dotnet run
us -> United States
de -> Germany
sk -> Slovakia
hu -> Hungary
jp -> Unknown

C# switch statement vs switch expression

The switch statement executes statements, while the switch expression produces a value.

Program.cs
int day = 3;

string name;

switch (day)
{
    case 1:
        name = "Monday";
        break;
    case 2:
        name = "Tuesday";
        break;
    case 3:
        name = "Wednesday";
        break;
    default:
        name = "Unknown";
        break;
}

Console.WriteLine(name);

string name2 = day switch
{
    1 => "Monday",
    2 => "Tuesday",
    3 => "Wednesday",
    _ => "Unknown"
};

Console.WriteLine(name2);

The program converts a day number to a day name, first with a switch statement and then with a switch expression. The switch expression does not need the case and break keywords, and the result is assigned directly to a variable.

$ dotnet run
Wednesday
Wednesday

C# switch expression type pattern

Data types can be patterns of the switch expression. A type pattern can also declare a variable which holds the converted value.

Program.cs
int age = 23;
string name = "Peter";
List<string> colors = ["blue", "khaki", "orange"];
int[] nums = [1, 2, 3, 4, 5];
double? weight = null;

Console.WriteLine(Check(age));
Console.WriteLine(Check(name));
Console.WriteLine(Check(colors));
Console.WriteLine(Check(nums));
Console.WriteLine(Check(3.5));
Console.WriteLine(Check(weight));

string Check(object? val) => val switch
{
    int => "integer",
    string s => $"string of length {s.Length}",
    List<string> l => $"list of {l.Count} strings",
    Array a => $"array of {a.Length} elements",
    null => "null value",
    _ => $"unknown type {val.GetType().Name}"
};

In the example, we find out the data type of a variable using a switch expression.

    string s => $"string of length {s.Length}",

If the value is a string, it is assigned to the s variable of type string, which is then used in the arm's expression.

    null => "null value",

The null constant pattern matches a null reference. A type pattern never matches null. The nullable double without a value is boxed into a null reference.

$ dotnet run
integer
string of length 5
list of 3 strings
array of 5 elements
unknown type Double
null value

C# switch expression relational pattern

Powerful logic can be constructed with relational patterns. They use the <, >, <=, and >= operators with constants.

Program.cs
List<int> nums = [-3, 2, 0, 1, 9, -2, 7];

foreach (var num in nums)
{
    var res = num switch
    {
        < 0 => "negative",
        0 => "zero",
        > 0 => "positive"
    };

    Console.WriteLine($"{num} is {res}");
}

We have a list of integers. In a foreach loop, we go through the list and print whether the value is negative, positive, or zero with a switch expression. Inside the switch expression, we use simple relational patterns. The three arms cover all integers, so no discard arm is needed.

$ dotnet run
-3 is negative
2 is positive
0 is zero
1 is positive
9 is positive
-2 is negative
7 is positive

C# switch expression when guards

Using when guards adds additional flexibility to our expression arms. The guard is a boolean expression which must be true for the arm to be selected.

Program.cs
List<User> users =
[
    new("John", "Doe", 34),
    new("Roger", "Roe", 44),
    new("Jane", "Doe", 44),
    new("Robert", "Molnar", 41),
    new("Lucia", "Petrovicova", 16),
];

foreach (var user in users)
{
    Console.WriteLine(CheckPerson(user));
}

string CheckPerson(User u) => u switch
{
    { LastName: "Doe" } => "Doe family",
    { Age: var age } when age < 18 => "minor person",
    { FirstName: var fname, Age: var age } when fname.StartsWith('R') && age > 40
        => $"{fname} is a senior R-member",
    _ => $"{u}"
};

record User(string FirstName, string LastName, int Age);

In the example, we use a when guard to account for users younger than 18.

    { Age: var age } when age < 18 => "minor person",

The var pattern captures the age into a variable, which is then checked in the guard.

    { FirstName: var fname, Age: var age } when fname.StartsWith('R') && age > 40
        => $"{fname} is a senior R-member",

A guard can contain any boolean expression, including method calls.

$ dotnet run
Doe family
Roger is a senior R-member
Doe family
Robert is a senior R-member
minor person

C# switch expression logical pattern

The and, or, and not pattern combinators can be used in switch expressions.

Program.cs
var today = new DateOnly(2026, 11, 6);

List<User> users =
[
    new("Peter Novak", "driver", new DateOnly(2000, 12, 1)),
    new("John Doe", "gardener", new DateOnly(1996, 2, 10)),
    new("Roger Roe", "teacher", new DateOnly(1961, 5, 9)),
    new("Lucia Smith", "student", new DateOnly(2010, 8, 18)),
    new("Roman Green", "retired", new DateOnly(1945, 7, 21)),
];

foreach (var user in users)
{
    int age = GetAge(user.Dob, today);

    string res = age switch
    {
        < 18 => "minor",
        >= 18 and < 65 => "adult",
        >= 65 => "senior",
    };

    Console.WriteLine($"{user.Name} ({age}) is {res}");
}

static int GetAge(DateOnly dob, DateOnly today)
{
    int age = today.Year - dob.Year;
    return dob > today.AddYears(-age) ? age - 1 : age;
}

record User(string Name, string Occupation, DateOnly Dob);

The logical and pattern is used to assign the adult string to users with age from 18 to 64. To keep the output stable, the age is computed relative to a fixed date.

    string res = age switch
    {
        < 18 => "minor",
        >= 18 and < 65 => "adult",
        >= 65 => "senior",
    };

The ranges cover all integers without gaps, so the compiler knows that the switch expression is exhaustive.

$ dotnet run
Peter Novak (25) is adult
John Doe (30) is adult
Roger Roe (65) is senior
Lucia Smith (16) is minor
Roman Green (81) is senior

C# switch expression or and not patterns

In the following example, we use or and not patterns.

Program.cs
int[] answers = [1, 2, 3, 5];

foreach (var ans in answers)
{
    string res = ans switch
    {
        1 or 3 => "incorrect",
        2 => "correct",
        _ => "unknown option"
    };

    Console.WriteLine($"WWI started in option {ans}: {res}");
}

char[] chars = ['a', 'Z', '7', '?'];

foreach (var c in chars)
{
    string kind = c switch
    {
        >= 'a' and <= 'z' or >= 'A' and <= 'Z' => "letter",
        >= '0' and <= '9' => "digit",
        not (' ' or '\t') => "other",
        _ => "whitespace"
    };

    Console.WriteLine($"{c} is {kind}");
}

The example evaluates answers to a quiz question and classifies characters.

        1 or 3 => "incorrect",

The arm matches if the value is either 1 or 3.

        >= 'a' and <= 'z' or >= 'A' and <= 'Z' => "letter",

The and combinator has higher precedence than or. The pattern matches lowercase and uppercase ASCII letters.

        not (' ' or '\t') => "other",

The not pattern negates a pattern. Parentheses group patterns.

$ dotnet run
WWI started in option 1: incorrect
WWI started in option 2: correct
WWI started in option 3: incorrect
WWI started in option 5: unknown option
a is letter
Z is letter
7 is digit
? is other

C# switch expression property pattern

The values of object properties can be patterns in a switch expression.

Program.cs
List<Product> products =
[
    new("Product A", 70m, 1, 10),
    new("Product B", 50m, 3, 15),
    new("Product C", 35m, 2, 20)
];

foreach (var product in products)
{
    decimal sum = product switch
    {
        { Quantity: 2 } =>
            product.Price * product.Quantity * (1 - product.Discount / 100m),
        _ => product.Price * product.Quantity,
    };

    Console.WriteLine($"The final sum for {product.Name} is {sum}");
}

record Product(string Name, decimal Price, int Quantity, int Discount);

In the example, we apply a discount to the price of a product if we have bought two items of the product. Twenty percent discount was applied to Product C.

$ dotnet run
The final sum for Product A is 70
The final sum for Product B is 150
The final sum for Product C is 56.0

C# switch expression nested property pattern

Property patterns can be nested. Since C# 10, we can also use the extended property pattern, which accesses nested properties with a dot.

Program.cs
List<Order> orders =
[
    new(1, new Customer("John", new Address("Bratislava", "SK"))),
    new(2, new Customer("Paul", new Address("Vienna", "AT"))),
    new(3, new Customer("Anna", new Address("Kosice", "SK"))),
    new(4, new Customer("Lucy", new Address("Berlin", "DE"))),
];

foreach (var order in orders)
{
    string shipping = order switch
    {
        { Customer.Address.Country: "SK" } => "domestic",
        { Customer.Address: { Country: "AT" or "DE" } } => "EU neighbour",
        _ => "international"
    };

    Console.WriteLine($"Order {order.Id} for {order.Customer.Name}: {shipping}");
}

record Address(string City, string Country);
record Customer(string Name, Address Address);
record Order(int Id, Customer Customer);

We determine the shipping type of orders based on the customer's country.

        { Customer.Address.Country: "SK" } => "domestic",
        { Customer.Address: { Country: "AT" or "DE" } } => "EU neighbour",

The first arm uses the extended property pattern, the second one nests a property pattern inside another. Both forms are equivalent; the extended pattern is more readable.

$ dotnet run
Order 1 for John: domestic
Order 2 for Paul: EU neighbour
Order 3 for Anna: domestic
Order 4 for Lucy: EU neighbour

C# switch expression positional pattern

A positional pattern deconstructs the value and matches its parts. Records with primary constructors provide a Deconstruct method automatically.

Program.cs
Point[] points = [new(0, 0), new(3, 0), new(0, -2), new(4, 4), new(2, 5)];

foreach (var p in points)
{
    string pos = p switch
    {
        (0, 0) => "origin",
        (_, 0) => "on the x-axis",
        (0, _) => "on the y-axis",
        var (x, y) when x == y => "on the diagonal",
        var (x, y) => $"at {x}, {y}"
    };

    Console.WriteLine($"{p} is {pos}");
}

record Point(int X, int Y);

We determine the position of points in a plane.

        (_, 0) => "on the x-axis",

The pattern matches points whose Y coordinate is zero. The discard ignores the X coordinate.

        var (x, y) when x == y => "on the diagonal",

The coordinates are deconstructed into variables and compared in a guard.

$ dotnet run
Point { X = 0, Y = 0 } is origin
Point { X = 3, Y = 0 } is on the x-axis
Point { X = 0, Y = -2 } is on the y-axis
Point { X = 4, Y = 4 } is on the diagonal
Point { X = 2, Y = 5 } is at 2, 5

C# switch expression tuple pattern

A tuple pattern matches several values at once.

Program.cs
string[] options = ["rock", "paper", "scissors"];
var rnd = new Random(17);

for (int i = 0; i < 5; i++)
{
    string human = options[rnd.Next(options.Length)];
    string computer = options[rnd.Next(options.Length)];

    Console.WriteLine($"I have {computer}, you have {human}");
    Console.WriteLine(RockPaperScissors(human, computer));
}

string RockPaperScissors(string human, string computer) => (human, computer) switch
{
    ("rock", "paper") => "Rock is covered by paper. You lose.",
    ("rock", "scissors") => "Rock breaks scissors. You win.",
    ("paper", "rock") => "Paper covers rock. You win.",
    ("paper", "scissors") => "Paper is cut by scissors. You lose.",
    ("scissors", "rock") => "Scissors are broken by rock. You lose.",
    ("scissors", "paper") => "Scissors cut paper. You win.",
    _ => "Tie."
};

We play several rounds of the rock paper scissors game. The random generator has a fixed seed, so the output is the same on every run.

string RockPaperScissors(string human, string computer) => (human, computer) switch
{
    ("rock", "paper") => "Rock is covered by paper. You lose.",
    ("rock", "scissors") => "Rock breaks scissors. You win.",

From the human and computer choices we form tuples which are used as patterns in the switch expression to reach the game conclusion. The discard arm covers all the ties.

$ dotnet run
I have scissors, you have paper
Paper is cut by scissors. You lose.
I have rock, you have paper
Paper covers rock. You win.
I have paper, you have scissors
Scissors cut paper. You win.
I have scissors, you have paper
Paper is cut by scissors. You lose.
I have paper, you have rock
Rock is covered by paper. You lose.

C# switch expression list patterns

Since C# 11, switch expression arms can evaluate list patterns.

Program.cs
List<Student> students = [
    new("John", ['a', 'b', 'a', 'c', 'd', 'd', 'e']),
    new("Lucia", ['b', 'b', 'c', 'c', 'a', 'e', 'e']),
    new("Paul", ['a', 'b', 'a', 'c', 'b', 'e', 'e']),
    new("Roger", ['a', 'c', 'c', 'c', 'a', 'b', 'e']),
    new("Michal", ['b', 'b', 'c', 'c', 'a', 'e', 'e']),
];

foreach (var student in students)
{
    string res = student.Answers switch
    {
        ['b', 'b', 'c', 'c', 'a', 'e', 'e'] => $"{student.Name} has passed",
        _ => $"{student.Name} has failed"
    };

    Console.WriteLine(res);
}

record Student(string Name, List<char> Answers);

Students are having a test with seven questions. The questions have answers 'a' through 'e'. To pass a test, all questions must be answered correctly.

    string res = student.Answers switch
    {
        ['b', 'b', 'c', 'c', 'a', 'e', 'e'] => $"{student.Name} has passed",
        _ => $"{student.Name} has failed"
    };

We check results against the correct one. The pattern is a list of characters that represent correct answers.

$ dotnet run
John has failed
Lucia has passed
Paul has failed
Roger has failed
Michal has passed

C# switch expression slice pattern

List patterns become more flexible with the slice pattern .., which matches zero or more elements.

Program.cs
int[][] data = [[], [7], [1, 2], [1, 5, 9], [4, 0, 0, 0, 8], [2, 3, 4, 5]];

foreach (var vals in data)
{
    string desc = vals switch
    {
        [] => "empty",
        [var x] => $"single value {x}",
        [var first, var last] => $"pair {first} and {last}",
        [1, ..] => "starts with one",
        [var first, .. var mid, var last] when mid.All(e => e == 0)
            => $"{first} and {last} with {mid.Length} zeros between",
        [_, .., var last] => $"ends with {last}"
    };

    Console.WriteLine($"[{string.Join(", ", vals)}]: {desc}");
}

We describe several arrays with list patterns.

        [] => "empty",
        [var x] => $"single value {x}",

The empty list pattern matches an empty array; [var x] matches an array with exactly one element.

        [1, ..] => "starts with one",

The slice pattern matches any number of remaining elements.

        [var first, .. var mid, var last] when mid.All(e => e == 0)

A slice can be captured into a variable. For arrays, the captured slice is a new array.

$ dotnet run
[]: empty
[7]: single value 7
[1, 2]: pair 1 and 2
[1, 5, 9]: starts with one
[4, 0, 0, 0, 8]: 4 and 8 with 3 zeros between
[2, 3, 4, 5]: ends with 5

C# switch expression exhaustiveness

The compiler checks whether the arms of a switch expression cover all possible input values. If they do not, it issues the CS8509 warning. If no arm matches at runtime, a SwitchExpressionException is thrown.

Program.cs
Console.WriteLine(GetName(2));

try
{
    Console.WriteLine(GetName(5));
}
catch (System.Runtime.CompilerServices.SwitchExpressionException e)
{
    Console.WriteLine(e.Message);
}

#pragma warning disable CS8509
string GetName(int day) => day switch
{
    1 => "Monday",
    2 => "Tuesday",
    3 => "Wednesday"
};

The switch expression handles only three values. We disable the warning to demonstrate the runtime behaviour; in real code, we add a discard arm or an arm that throws a more descriptive exception.

$ dotnet run
Tuesday
Non-exhaustive switch expression failed to match its input.
Unmatched value was 5.

Source

switch expression - pattern matching expressions using the switch keyword

In this article we have presented C# switch expression.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.