How to Use Pattern Matching Switch Expressions in C

Pattern matching with switch expressions in C# provides a concise, functional approach to conditional branching based on an object's type, properties, or values. Introduced in C# 8.0 and expanded in subsequent versions, switch expressions replace verbose switch statements with lightweight expressions that return values. This article covers the essential syntax and the most common pattern-matching techniques—including type, relational, logical, property, and positional patterns—to help you write cleaner and more expressive C# code.

Basic Syntax and the Discard Pattern

A switch expression evaluates an input expression and matches it against a series of comma-separated arms. Each arm consists of a pattern, a case guard (optional), the => operator, and an expression to evaluate. The underscore (_) represents the discard pattern, which acts as the default case.

int dayNumber = 3;

string dayName = dayNumber switch
{
    1 => "Monday",
    2 => "Tuesday",
    3 => "Wednesday",
    4 => "Thursday",
    5 => "Friday",
    _ => "Weekend"
};

Type Patterns and Declaration Patterns

Type patterns check whether a value can be cast to a specified type. A declaration pattern checks the type and simultaneously binds the cast value to a new variable for use in the arm.

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

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

Relational and Logical Patterns

Introduced in C# 9.0, relational patterns let you compare values using <, <=, >, and >=. Logical patterns use the keywords and, or, and not to combine or invert patterns.

public string GetTemperatureDescription(double temperature) => temperature switch
{
    < 0 => "Freezing",
    >= 0 and <= 15 => "Cold",
    > 15 and <= 25 => "Comfortable",
    > 25 and < 35 => "Warm",
    >= 35 => "Hot"
};

Property Patterns

Property patterns match when an object's properties or fields match specified values or sub-patterns. You can evaluate multiple properties in a single pattern.

public record Order(decimal Subtotal, bool IsExpedited, string Country);

public decimal CalculateShipping(Order order) => order switch
{
    { Subtotal: >= 100m, Country: "US" } => 0m,
    { IsExpedited: true, Country: "US" } => 15m,
    { Country: "US" } => 5m,
    { Country: not "US" } => 25m,
    _ => 10m
};

Positional and Tuple Patterns

Positional patterns match values based on deconstruction. You can use them directly on tuples or any type that defines a Deconstruct method.

public string EvaluateCoordinates(int x, int y) => (x, y) switch
{
    (0, 0) => "Origin",
    (0, _) => "On Y-axis",
    (_, 0) => "On X-axis",
    ( > 0, > 0) => "Quadrant 1",
    ( < 0, > 0) => "Quadrant 2",
    ( < 0, < 0) => "Quadrant 3",
    ( > 0, < 0) => "Quadrant 4"
};

Case Guards with when

When a pattern alone cannot express the required condition, use the when keyword to specify a boolean condition that must also evaluate to true for the arm to match.

public decimal ApplyDiscount(decimal price, int customerLoyaltyYears) => (price, customerLoyaltyYears) switch
{
    (var p, > 5) when p > 100m => p * 0.80m, // 20% discount
    (var p, > 2) when p > 50m => p * 0.90m,  // 10% discount
    (var p, _) => p                          // No discount
};