Skip to Content
Course content

14: Pattern Matching in Switch Expressions

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact bug land in pull requests more times than I can count. A developer wants to clean up a messy if-else chain by using the newer C# switch expressions, but they forget one critical detail: the compiler is a lot more strict about "exhaustiveness" than it is with the old-school switch statements.

public decimal GetProcessingFee(PaymentMethod method) => method switch
{
    PaymentMethod.CreditCard => 2.50m,
    PaymentMethod.PayPal => 1.20m,
    PaymentMethod.BankTransfer => 0.00m
};

The Crash You Didn't See Coming

On the surface, this looks clean. If PaymentMethod is an enum with those three values, it works perfectly. But imagine a teammate adds PaymentMethod.Crypto to the enum a month later. This code doesn't have a compile-time error—it compiles just fine. However, the moment a Crypto payment hits this method, the application will throw a SwitchExpressionException and crash.

The old switch statement just silently does nothing if no case matches. The switch expression, because it must return a value, cannot just "do nothing." If it reaches the end without a match, it panics. I always tell my juniors: if you're using a switch expression, you are making a contract with the compiler that every possible input is handled.

Closing the Gap with the Discard Pattern

To fix this, we use the discard pattern (the underscore _). This acts as the default case from the traditional switch statement. It ensures that no matter what happens to your data models in the future, the method always returns a value.

public decimal GetProcessingFee(PaymentMethod method) => method switch
{
    PaymentMethod.CreditCard => 2.50m,
    PaymentMethod.PayPal => 1.20m,
    PaymentMethod.BankTransfer => 0.00m,
    _ => throw new ArgumentOutOfRangeException(nameof(method), $"Unsupported payment method: {method}")
};

Now, instead of a generic SwitchExpressionException, we provide a meaningful error. Or, if a default fee makes sense, you could just return 5.00m. The point is, the _ ensures the expression is exhaustive.

Matching on More Than Just Values

The real power of this syntax isn't just replacing enums; it's pattern matching. You can check types, ranges, and properties all in one go. I personally find this much more readable than nesting three if statements inside each other.

Let's say we're calculating a shipping discount based on an Order object. We can match based on the property values directly inside the switch:

public decimal GetShippingDiscount(Order order) => order switch
{
    { TotalAmount: > 100 } => 10.00m,             // Relational pattern
    { TotalAmount: > 50 } => 5.00m,              // Relational pattern
    { Customer.IsPremium: true } => 2.00m,       // Property pattern (deep dive)
    _ => 0.00m                                   // The safety net
};

Notice how I didn't have to write order.TotalAmount > 100. The { TotalAmount: > 100 } syntax tells C#: "If this object has a property called TotalAmount and it's greater than 100, use this arm." It's concise, but it keeps the logic focused on the data rather than the plumbing.

Adding Precision with Guard Clauses

Sometimes a simple property match isn't enough. You might need to call a method or perform a complex calculation to decide the result. That's where the when keyword comes in. Think of it as a filter for your pattern.

public string GetShippingPriority(Order order) => order switch
{
    { TotalAmount: > 500 } => "Overnight",
    { TotalAmount: > 100 } when order.IsInternational => "Priority Air",
    { TotalAmount: > 100 } => "Standard Ground",
    _ => "Economy"
};

The order here is vital. C# evaluates these from top to bottom. If I put the "Standard Ground" case above the "Priority Air" case, the international check would never be reached because any amount over 100 would have already matched. Always put your most specific conditions at the top and your most general ones at the bottom.




📋 Practical Task

Refactoring the User Access Level Validator

You are working on a legacy permissions system. Currently, the code uses a clunky set of if/else statements to determine what a user can do based on their User object. Your task is to refactor this into a modern switch expression.

Requirements:

  • If the user is an Admin, they get "FullAccess".
  • If the user is a Moderator AND their AccountAgeDays is greater than 30, they get "ModeratedAccess".
  • If the user is a Moderator but their account is 30 days old or younger, they get "LimitedModeratorAccess".
  • If the user is a Guest, they get "ReadOnly".
  • Any other case (including nulls or undefined roles) should return "NoAccess".
public class User 
{
    public string Role { get; set; } // "Admin", "Moderator", "Guest"
    public int AccountAgeDays { get; set; }
}

public class AccessControl
{
    public string GetPermissions(User user)
    {
        // TODO: Replace the logic below with a switch expression
        if (user == null) return "NoAccess";
        if (user.Role == "Admin") return "FullAccess";
        if (user.Role == "Moderator")
        {
            if (user.AccountAgeDays > 30) return "ModeratedAccess";
            return "LimitedModeratorAccess";
        }
        if (user.Role == "Guest") return "ReadOnly";
        
        return "NoAccess";
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.