Published on

C# Pattern Matching

Authors

Rethinking Pattern Matching

Pattern matching is often associated with switch and case. However modern C# turns pattern matching into something that goes far beyond branching.

It is not just control flow anymore. It has become expressive, and type safety has become a thing, which makes the language as a whole more robust.


Type Patterns

Instead of casting, you can now just match and declare in one step.

class Customer
{
    public string Name { get; set; } = "";
}

class Program
{
    static void Main()
    {
        object obj = new Customer { Name = "Alice" };
        if (obj is Customer c)
        {
            Console.WriteLine($"Hello {c.Name}");
        }
    }
}
  • Language version: C# 7.0
  • Why: No need for redundant casting and null checks. This makes type checks concise and safe.

Property Patterns

You can match against properties directly:

enum OrderStatus { Pending, Shipped, Cancelled }

class Order
{
    public decimal Total { get; set; }
    public OrderStatus Status { get; set; }
}

class Program
{
    static void Main()
    {
        var order = new Order { Total = 150, Status = OrderStatus.Pending };

        if (order is { Total: > 100, Status: OrderStatus.Pending })
        {
            Console.WriteLine("High-value pending order!");
        }
    }
}
  • Language version: C# 8.0
  • Why: Let you match objects by their object shape as well as property values without writing multiple nested if statements. This is inspired by F# and other functional languages.

Switch Expressions

The new switch expression makes pattern matching feel like functional programming:

enum OrderStatus { Pending, Shipped, Cancelled }

class Order
{
    public OrderStatus Status { get; set; }
}

class Program
{
    static void Main()
    {
        var order = new Order { Status = OrderStatus.Shipped };

        string message = order switch
        {
            { Status: OrderStatus.Shipped } => "On its way 🚚",
            { Status: OrderStatus.Cancelled } => "Order cancelled ❌",
            _ => "Processing..."
        };

        Console.WriteLine(message);
    }
}
  • Language version: C# 8.0
  • Why: A declarative and concise way to write switch logic. Ensures exhaustiveness, reduces ceremony, and makes branching feel functional.

Recursive Patterns

Patterns can go deep into object graphs:

class Node
{
    public int Value { get; set; }
    public Node? Left { get; set; }
    public Node? Right { get; set; }
}

class Program
{
    static void Main()
    {
        var tree = new Node
        {
            Left = new Node { Value = 0 },
            Right = new Node { Value = 1 }
        };

        if (tree is { Left: { Value: 0 }, Right: { Value: 1 } })
        {
            Console.WriteLine("Found the shape we’re looking for!");
        }
    }
}
  • Language version: C# 8.0 (expanded in C# 9.0)
  • Why: This enables matching on deeply nested object graphs.

List Patterns (C# 11+)

You can now break apart lists and arrays using patterns. For example, the code below checks if the first item is 0. If it is, the remaining elements (1, 2, 3, 4) are stored in the rest variable.

class Program
{
    static void Main()
    {
        int[] numbers = { 0, 1, 2, 3, 4 };

        if (numbers is [0, .. var rest])
        {
            Console.WriteLine($"Starts with zero. Remaining: {string.Join(",", rest)}");
        }
    }
}
  • Language version: C# 11.0
  • Why: This feature extends pattern matching to List Patters, enabling functional-style list destructuring ([head, ..tail]).

But why?

It expressed the intent directly, the code is safer (compile checks) and code becomes more declarative. The feature from a birds eye persprective might seem small, but that is only until you start using it everywhere. It simplifies things like null checks, branching, obejct inspection and collection handling.