Switch statement has changed quite a lot since the early versions of the language. I can't keep track of the changes.

Today, you can use switch to match not only values, but also types, properties, collections, and more.

Let's look at how we got here.

1. The original switch

The switch statement has been part of C# since C# 1.0.

Originally, its job was quite straightforward: take a value and execute a block of code depending on which constant it matches.

Imagine an application processing an order status:

Role status = Role.User;

switch (status)
{
    case Role.Admin:
        // Execute logic...
        break;

    case Role.Editor:
        // Execute logic...
        break;

    case Role.Viewer:
        // Execute logic...
        break;

    default:
        // Execute logic...
        break;
}

Each case represented a value.

It was useful, but fairly limited.

You couldn't say:

If the number is greater than 100.

or:

If this object is a Customer.

The cases were primarily about matching known values.

There was also a familiar break at the end of each case.

2. string made switch more practical

As C# evolved, switch became useful with more types, including string.

For example:

string role = "Admin";

switch (role)
{
    case "Admin":
        // Execute logic...
        break;

    case "Editor":
        // Execute logic...
        break;

    case "Viewer":
        // Execute logic...
        break;

    default:
        // Execute logic...
        break;
}

This is probably the version of switch many developers first encountered.

But eventually developers wanted to express something more complicated than:

Does this value equal X?

They wanted to ask:

What type is this?

Does this object have these properties?

Does this collection have this shape?

That's where pattern matching changed the story.

3. C# 7: switch starts understanding patterns

This was one of the biggest changes in the history of switch.

Instead of only asking:

Does this value equal this constant?

you could start asking:

Does this value have this type?

For example:

object value = "Hello";

switch (value)
{
    case string text:
        // Execute logic...
        break;

    case int number:
        // Execute logic...
        break;

    case DateTime date:
        // Execute logic...
        break;

    default:
        // Execute logic...
        break;
}

Notice what happened here.

We're not comparing:

value == "Hello"

We're asking what value actually is.

This was a major shift in how developers could think about switch.

4. When allowed additional conditions

Pattern matching became even more useful when C# allowed conditions with when.

For example, suppose we receive different kinds of messages:

object message = 150;

switch (message)
{
    case int number when number > 100:
        // Execute logic...
        break;

    case int number:
        // Execute logic...
        break;

    default:
        // Execute logic...
        break;
}

Now switch isn't simply checking whether the value is an int.

It's checking:

Is it an int?
    Is it greater than 100?
       yes -> Large number

The when clause adds another condition to the pattern. Modern C# still supports this approach.

5. C# 8: the switch expression

Then came another major change: the switch expression.

This is probably the change that made modern switch look completely different from the old version.

Previously, if we wanted to convert an order status into a message, we might write:

string GetMessage(Role role)
{
    switch (role)
    {
        case Role.Viewer:
            return // Execute logic...

        case Role.Admin:
            return // Execute logic...

        case Role.Editor:
            return // Execute logic...

        default:
            return // Execute logic...
    }
}

C# 8 introduced a much more concise way of expressing the same idea:

string GetMessage(Role role)
{
    return role switch
    {
        Role.Editor=> "Is Editor",
        Role.Admin => "Is Admin",
        Role.Viewer => "Is Viewer",
        _ => "Is User"
    };
}

This is called a switch expression because it produces a value.

6. C# 9: relational and logical patterns

We are not done yet, the evolution didn't stop there.

C# 9 introduced relational patterns such as:

< 18
>= 18
> 100
<= 500

and logical patterns such as:

and
or
not

7. Property patterns

Now things get more interesting.

Suppose we have:

public class Customer
{
    public string Country { get; set; }
    public int Age { get; set; }
}

We can use a switch to inspect the object's properties.

string GetCustomerCategory(Customer customer) =>
    customer switch
    {
        { Country: "Tunisia", Age: >= 18 } => "Adult customer from Tunisia",
        { Country: "France", Age: >= 18 } => "Adult customer from France",
        { Age: < 18 } => "Minor",
        _ => "Other"
    };

Notice that we're no longer switching on a single value.

We're matching the shape and properties of an object.

This is called a property pattern.

8. C# 11: list patterns

Another interesting addition was list patterns.

You can now match the structure of an array or collection.

For example:

string Describe(int[] values) =>
    values switch
    {
        [] => "Empty",
        [1, 2, 3] => "Exactly 1, 2, 3",
        [1, ..] => "Starts with 1",
        [.., 5] => "Ends with 5",
        _ => "Something else"
    };

9. switch statement vs switch expression

This is something I think is worth making clear because the terminology can be confusing.

They're related, but they're not the same thing.

A switch statement:

switch (status)
{
    case OrderStatus.Paid:
        SendEmail();
        UpdateDatabase();
        break;

    case OrderStatus.Cancelled:
        Refund();
        break;
}

is useful when each branch needs to execute one or more operations.

A switch expression:

var message = status switch
{
    OrderStatus.Paid => "Payment received",
    OrderStatus.Cancelled => "Order cancelled",
    _ => "Unknown"
};

is useful when the main purpose is to calculate a value.