Skip to content

Pattern matching

How pattern matching works and how it has evolved across C# versions.

Pattern matching lets you test a value against a shape (a type, a set of properties, a range, a sequence) and extract data from it in one step. It has grown from a simple type-check shorthand in C# 7.0 into a comprehensive system for expressing complex conditional logic declaratively.

The basics

A pattern appears wherever the language expects a test: after is in an expression, in the arms of a switch expression, or in the cases of a switch statement. The compiler checks the value against the pattern and, if it matches, optionally binds variables you can use in the body.

C#
// Type pattern: test and cast in one step
if (shape is Circle c)
    Console.WriteLine($"Radius: {c.Radius}");

// Constant pattern: match a literal value
if (statusCode is 404)
    Console.WriteLine("Not found");

// var pattern: always matches, captures the value
if (GetResult() is var result && result > 0)
    Console.WriteLine(result);

Switch expressions

Switch expressions C# 8.0 provide a concise way to produce a value from a set of patterns. Each arm maps a pattern to a result, and the discard _ serves as the default.

C#
string Describe(object obj) => obj switch
{
    int n when n < 0    => "negative integer",
    int n               => $"integer: {n}",
    string s            => $"string of length {s.Length}",
    null                => "null",
    _                   => "something else"
};

Traditional switch statements also support patterns, which is useful when each case needs to execute multiple statements rather than return a value.

Pattern kinds

The table below lists every pattern kind and the version it was introduced. They can be freely combined: a property pattern can contain relational patterns, a list pattern can contain type patterns, and so on.

PatternSinceExample
Type7.0is string s
Constant7.0is 42, is null
Var7.0is var x
Discard7.0_
Generic type7.1is T t (in generic methods)
Positional8.0is (int x, int y)
Property8.0is { Length: > 0 }
Tuple8.0(a, b) switch { (true, true) => ... }
Relational9.0is > 0 and < 100
Logical (and, or, not)9.0is not null
Simplified type9.0is int (without variable)
Extended property10.0is { Address.City: "London" }
List11.0is [1, .., > 0]
Span on string11.0span is "hello"

Combining patterns

Patterns compose naturally. The logical keywords and, or, and not (C# 9.0) let you build complex conditions that remain readable.

C#
string ClassifyTemperature(double temp) => temp switch
{
    < 0                  => "freezing",
    >= 0 and < 15        => "cold",
    >= 15 and < 25       => "comfortable",
    >= 25 and < 35       => "warm",
    >= 35                => "hot"
};

bool IsValidInput(object obj) => obj is not null and not "";

Property and positional patterns

Property patterns C# 8.0 match against an object's properties. Extended property patterns C# 10.0 allow dot-notation for nested access.

C#
// Property pattern
if (order is { Status: "shipped", Items.Count: > 0 })
    Notify(order);

// Positional pattern, works with types that have Deconstruct
if (point is (0, 0))
    Console.WriteLine("Origin");

Positional patterns work with any type that has a Deconstruct method, including tuples and records C# 9.0.

Tuple patterns

Tuple patterns C# 8.0 let you match on multiple values at once without nesting if statements. This is especially useful for state machines.

C#
string RockPaperScissors(string a, string b) => (a, b) switch
{
    ("rock", "scissors")  => "a wins",
    ("scissors", "paper") => "a wins",
    ("paper", "rock")     => "a wins",
    (_, _) when a == b    => "tie",
    _                     => "b wins"
};

List patterns

List patterns C# 11.0 match sequences by position. The slice pattern .. matches zero or more elements and can capture them into a variable.

C#
string Describe(int[] arr) => arr switch
{
    []              => "empty",
    [var only]      => $"single: {only}",
    [0, ..]         => "starts with zero",
    [.., < 0]       => "ends negative",
    [_, .. var mid, _] => $"middle has {mid.Length} elements"
};

List patterns work with any type that is countable and indexable, including arrays, List<T>, and Span<T>.

Guards

Any pattern arm can add a when clause for conditions that cannot be expressed as a pattern.

C#
string Categorize(int[] numbers) => numbers switch
{
    [var first, ..] when first == numbers[^1] => "first equals last",
    { Length: > 100 }                         => "large collection",
    _                                         => "other"
};

Tips

  • Exhaustiveness. The compiler warns when a switch expression does not cover all possible inputs. Add a discard _ arm or ensure all cases are handled.
  • Order matters. Arms are evaluated top to bottom. Place more specific patterns before more general ones, or the compiler will flag unreachable arms.
  • Performance. The compiler optimizes pattern matching into efficient branching. Prefer patterns over hand-written if/else chains; they are usually both clearer and faster.
  • Null. The constant pattern null and the not null pattern are the idiomatic way to handle null checks in pattern-matching code. A type pattern like is string s does not match null.