Menu

C# Pattern Matching: is, switch Patterns, Property and List Patterns

How pattern matching works in C#: the is operator with type and constant patterns, switch statements with case patterns and when, switch expressions, property, tuple and positional patterns, relational and logical patterns (and, or, not), list patterns, and which C# version added each one.

This page includes runnable editors - edit, run, and see output instantly.

Pattern matching tests a value against a shape: "is it a Circle?", "is it null?", "is its Status "paid" and its Total over 100?". When the test succeeds, the pattern can also extract parts of the value into new variables. It started small in C# 7 with the is operator and grew in every version since, so this page notes the version of each form.

The is type pattern (C# 7)

Before C# 7, checking a type and using it took two steps: as plus a null check, or is plus a cast. The type pattern does both at once and gives you a typed variable:

Output:

card ending 4242: 49.90
transfer from PT50...: 1200
no payment
other payment: 5

p is CardPayment card is true only when p is a non-null CardPayment (or a class derived from it), and then card holds it with the right static type. A null value never matches a type pattern, which is why the null payment falls through to the null check.

Scope and definite assignment

The pattern variable exists in the enclosing block, but the compiler lets you read it only where the match is known to have succeeded. That makes the "guard clause" shape work, where you bail out early and use the variable afterwards:

Output:

19.99
42 is over 40
price must be text

boxed is int n && n > 40 shows the other common shape: the variable is usable on the right of &&, because that side runs only when the match succeeded. On the right of || it is not assigned, and the compiler says so (CS0165).

Patterns also unbox: boxed is int n succeeds for a boxed int and gives you the value without a cast that could throw. For nullable value types, maybe is int n succeeds exactly when maybe has a value.

Constant and null patterns

A constant is a pattern too. x is null is the most used one, and C# 7 also allows x is 0, status is "paid" or an enum member:

if (customer is null) return;          // null check that ignores any == overload
if (count is 0) Console.WriteLine("empty");
if (level is LogLevel.Error) Alert();

is null is preferred over == null by many teams because it cannot be redirected by an operator overload on the type. C# 9 adds the negated form, is not null.

Patterns in switch statements (C# 7)

The same type patterns can label cases, and a when clause adds a condition. Cases are tried top to bottom, so order them from specific to general:

static string Describe(object o)
{
    switch (o)
    {
        case null:                           return "nothing";
        case int n when n < 0:               return "negative number";
        case int n:                          return $"number {n}";
        case string s when s.Length == 0:    return "empty text";
        case string s:                       return $"text of {s.Length} chars";
        case IEnumerable<int> list:          return "a sequence of numbers";
        default:                             return o.GetType().Name;
    }
}

Describe(-4);                 // negative number
Describe("");                 // empty text
Describe(new List<int>());    // a sequence of numbers
Describe(2.5);                // Double

The compiler checks the order: a case that can never match because an earlier one covers it is error CS8120. default is always evaluated last, wherever it appears. The same logic written with if/is chains, as in the payment example above, works in every C# version from 7 on.

Switch expressions (C# 8)

A switch expression is the compact form for "compute one value from several cases". The value comes first, then switch, then arms of pattern => result separated by commas, with _ as the catch-all:

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

static double Area(Shape shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle { Width: var w, Height: var h } => w * h,
    _ => throw new ArgumentException("Unknown shape", nameof(shape)),
};

Area(new Rectangle(3, 4));   // 12

If no arm matches at run time, it throws SwitchExpressionException, and the compiler warns (CS8509) when it can see that some input is unhandled. The switch expression page covers the syntax on its own; the rest of this page is about the patterns you can put in the arms.

Property patterns (C# 8, extended in C# 10)

A property pattern matches an object's properties against nested patterns, in braces. The object must also be non-null, so { } alone means "not null":

public record Address(string City, string Country);
public record Order(decimal Total, string Status, Address ShipTo, int Items);

static string Shipping(Order order) => order switch
{
    { Status: "cancelled" }                   => "no shipment",
    { ShipTo.Country: "PT", Total: >= 50m }   => "free, domestic",     // C# 10 dotted form
    { ShipTo.Country: "PT" }                  => "4.90, domestic",
    { Total: > 200m }                         => "free, international",
    _                                         => "12.00, international",
};

Shipping(new Order(60m, "paid", new Address("Porto", "PT"), 2));    // free, domestic
Shipping(new Order(260m, "paid", new Address("Lyon", "FR"), 2));    // free, international

In C# 8 and 9 the nested form is required: { ShipTo: { Country: "PT" } }. C# 10 allows the dotted shorthand used above. Property patterns work with is too, which makes multi-condition checks read like a description: if (order is { Status: "paid", Items: > 0 }).

Relational and logical patterns (C# 9)

C# 9 added comparisons (<, <=, >, >=) and the combinators and, or and not, which turn ranges into patterns:

static string Grade(int score) => score switch
{
    < 0 or > 100      => "invalid",
    >= 90             => "A",
    >= 75 and < 90    => "B",
    >= 50             => "C",
    _                 => "F",
};

Grade(82);    // B
Grade(101);   // invalid

if (input is not null and not "") { ... }
if (c is >= 'a' and <= 'z' or >= 'A' and <= 'Z') { ... }   // and binds tighter than or

The equivalent in C# 7 is an if/else if chain or a switch with when clauses (case int s when s >= 90:).

Tuple and positional patterns (C# 8)

Switching on several values at once is done by switching on a tuple:

static string Quadrant(int x, int y) => (x, y) switch
{
    (0, 0)       => "origin",
    (> 0, > 0)   => "I",
    (< 0, > 0)   => "II",
    (< 0, < 0)   => "III",
    (> 0, < 0)   => "IV",
    _            => "on an axis",
};

Quadrant(-2, 5);   // II

A positional pattern does the same for any type with a Deconstruct method, records included: case Point(0, 0): or p is Rectangle(var w, var h) && w == h.

List patterns (C# 11)

List patterns match arrays and lists by their elements. .. matches any number of elements (a "slice"), and each element position can hold any other pattern:

static string Route(string[] args) => args switch
{
    []                         => "help",
    ["add", var item]          => $"add {item}",
    ["remove", var item, ..]   => $"remove {item}",
    [var cmd, ..]              => $"unknown command {cmd}",
};

Route(new[] { "add", "milk" });             // add milk
Route(new[] { "remove", "milk", "now" });   // remove milk

int[] nums = { 1, 2, 3, 4 };
if (nums is [1, .., var last]) Console.WriteLine(last);   // 4

Which version added what

PatternExampleVersion
Type pattern with a variableo is Circle cC# 7.0
Constant patternx is null, n is 0C# 7.0
var patternx is var vC# 7.0
Patterns in case labels, when guardscase int n when n < 0:C# 7.0
Switch expression, discard _x switch { ... }C# 8
Property pattern{ Status: "paid" }C# 8
Tuple pattern(x, y) switch { (0, 0) => ... }C# 8
Positional patternPoint(0, 0)C# 8
Relational pattern> 100, <= 0C# 9
Logical patternsnot null, >= 1 and <= 5, 'a' or 'b'C# 9
Bare type patterncase Circle:, Circle => ...C# 9
Extended property pattern{ ShipTo.City: "Porto" }C# 10
List pattern and slice[first, .., last]C# 11

Common mistakes

  • Ordering cases from general to specific. case Payment p: before case CardPayment c: makes the second unreachable (CS8120). Specific first.
  • Expecting a type pattern to match null. null is string s is false. Handle null with its own case or is null.
  • Reading the pattern variable where the match may have failed. After a is int n || ..., n is not assigned (CS0165).
  • Long type switches instead of virtual methods. If every new subclass means editing the same switch in five places, the behavior belongs in the class hierarchy. Patterns shine for data from outside (JSON shapes, messages, tuples of inputs) and for closed sets of types you control.
  • Forgetting the catch-all in a switch expression. An unmatched input throws at run time; read warning CS8509.

Frequently Asked Questions

What is pattern matching in C#?

Pattern matching tests a value against a shape and, when it matches, can pull parts of it out into variables in the same step. if (shape is Circle c) tests the type and gives you a typed c; order is { Status: "paid", Total: > 100 } tests properties. Patterns appear in is expressions, switch statements and switch expressions.

How do I use is with a variable in C#?

if (obj is Customer c) { ... } checks that obj is a Customer (not null) and assigns it to c, typed as Customer, inside the if. It replaces the older var c = obj as Customer; if (c != null) pair. The variable is definitely assigned only where the test is known to be true, so if (!(obj is Customer c)) return; leaves c usable after the if.

What is the difference between is null and == null in C#?

x is null always checks the reference itself. x == null calls the type's == operator if it overloads one, which usually gives the same answer but is not guaranteed to. For the opposite, write x != null, !(x is null), or x is not null from C# 9.

How does switch pattern matching with when work in C#?

A case label can hold a pattern plus a when guard: case Order o when o.Total > 100:. The case matches only if the pattern matches and the condition is true, and cases are tried top to bottom, so put the more specific ones first. The compiler reports CS8120 when a case can never be reached because an earlier one already covers it.

Which C# version added which patterns?

C# 7.0: type, constant and var patterns in is and case, plus when. C# 8: switch expressions, property, tuple and positional patterns. C# 9: relational (> 5), logical (and, or, not) and bare type patterns. C# 10: extended property patterns ({ Address.City: "Porto" }). C# 11: list patterns ([1, .., var last]).

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED