Skip to main content
C# intermediate Lesson 19 of 25

Pattern Matching in C#

is expressions, switch expressions, property patterns, positional patterns, and list patterns in C#.

Type Patterns with is

The is operator tests a value’s type and optionally binds it to a new variable:

object value = "Hello, C#!";

// Old style — cast then use
if (value is string)
{
    string s = (string)value;
    Console.WriteLine(s.ToUpper());
}

// Type pattern — combines check and cast
if (value is string s)
    Console.WriteLine(s.ToUpper());  // s is string, available in this scope

// Works for null checks
if (value is not null)
    Console.WriteLine("Not null");

// Nested with 'and'
if (value is string { Length: > 5 } longString)
    Console.WriteLine($"Long string: {longString}");

Constant Patterns

int statusCode = 404;

string message = statusCode switch
{
    200 => "OK",
    201 => "Created",
    400 => "Bad Request",
    401 => "Unauthorized",
    403 => "Forbidden",
    404 => "Not Found",
    500 => "Internal Server Error",
    _   => $"Unknown status: {statusCode}"
};

// With strings
string lang = "fr";
string greeting = lang switch
{
    "en" => "Hello",
    "fr" => "Bonjour",
    "de" => "Hallo",
    "es" => "Hola",
    _    => "Hi"
};

Relational Patterns

double bmi = 22.5;

string category = bmi switch
{
    < 18.5          => "Underweight",
    >= 18.5 and < 25 => "Normal",
    >= 25 and < 30   => "Overweight",
    >= 30            => "Obese",
    double.NaN       => "Invalid",
    _                => "Unknown"
};

// Combined with type pattern
object obj = 42;
bool isPositiveInt = obj is int n and > 0;

Property Patterns

Match against an object’s properties without first assigning it to a variable:

record Address(string Street, string City, string Country, string PostalCode);
record Customer(string Name, Address Address, bool IsPremium, decimal CreditLimit);

Customer customer = new("Alice", new("123 Main St", "London", "UK", "SW1A 1AA"), true, 5000m);

// Property pattern
bool isEligible = customer is
{
    IsPremium: true,
    CreditLimit: >= 1000,
    Address.Country: "UK"
};

// Switch with property patterns
decimal discount = customer switch
{
    { IsPremium: true, CreditLimit: >= 5000 } => 0.20m,
    { IsPremium: true }                       => 0.10m,
    { Address.Country: "UK" }                 => 0.05m,
    _                                         => 0m
};

// Nested property patterns
string zone = customer switch
{
    { Address: { Country: "US", PostalCode: var zip } } when zip.StartsWith("9")
        => "US West",
    { Address.Country: "US" }  => "US East",
    { Address.Country: "UK" }  => "Europe",
    _                          => "Other"
};

Positional Patterns

Positional patterns match against a type’s Deconstruct method. Records get one automatically.

record Point(int X, int Y);
record Size(int Width, int Height);

var point = new Point(3, -1);

string quadrant = point switch
{
    (0, 0)      => "Origin",
    (> 0, > 0)  => "Q1",
    (< 0, > 0)  => "Q2",
    (< 0, < 0)  => "Q3",
    (> 0, < 0)  => "Q4",
    (_, 0)      => "X-axis",
    (0, _)      => "Y-axis",
    _           => "Unknown"
};
Console.WriteLine(quadrant);  // Q4

// Deconstruct a tuple
(string role, int level) user = ("admin", 5);
string permission = user switch
{
    ("admin", >= 5)   => "Full access",
    ("admin", _)      => "Limited admin",
    ("editor", _)     => "Edit access",
    _                 => "Read only"
};

// Custom Deconstruct
public class Rectangle
{
    public int Width { get; init; }
    public int Height { get; init; }
    public void Deconstruct(out int width, out int height) => (width, height) = (Width, Height);
}

var rect = new Rectangle { Width = 10, Height = 5 };
string shape = rect switch
{
    (var w, var h) when w == h => "Square",
    (> 0, > 0)                 => "Valid rectangle",
    _                          => "Invalid"
};

List Patterns (C# 11)

List patterns match against the structure of a sequence:

int[] empty   = { };
int[] one     = { 1 };
int[] two     = { 1, 2 };
int[] many    = { 1, 2, 3, 4, 5 };

string Describe(int[] arr) => arr switch
{
    []             => "empty",
    [var x]        => $"single: {x}",
    [var x, var y] => $"pair: {x}, {y}",
    [1, 2, ..]     => "starts with 1, 2",
    [.., 5]        => "ends with 5",
    _              => $"{arr.Length} elements"
};

Console.WriteLine(Describe(empty));  // empty
Console.WriteLine(Describe(one));    // single: 1
Console.WriteLine(Describe(two));    // pair: 1, 2
Console.WriteLine(Describe(many));  // starts with 1, 2

// Capture the rest of the sequence
if (many is [var first, .. var rest])
    Console.WriteLine($"First: {first}, Rest: [{string.Join(", ", rest)}]");
// First: 1, Rest: [2, 3, 4, 5]

// Parse command-line args with list patterns
string[] args = { "run", "--port", "8080", "--env", "production" };
string result = args switch
{
    ["run", "--port", var port, ..] => $"Running on port {port}",
    ["build", ..] => "Building",
    [] => "No arguments",
    _ => "Unknown command"
};

var Pattern

The var pattern always matches and captures the value — useful in switch guards:

string? input = GetInput();

bool valid = input switch
{
    null          => false,
    var s when s.Length > 100 => false,   // too long
    var s when s.All(char.IsDigit) => true,
    _             => false
};

Combining Patterns

// 'and' — both must match
bool isShortPositive = value is int n and > 0 and < 100;

// 'or' — either must match
bool isEdgeValue = value is 0 or int.MaxValue;

// 'not' — negation
bool isNotNull = value is not null;
bool isNotString = value is not string;

// Complex combination
object result = GetResult();
string summary = result switch
{
    null                           => "null result",
    string s and { Length: 0 }     => "empty string",
    string s                       => $"string: {s}",
    int n and (< 0 or > 1000)      => $"out of range int: {n}",
    int n                          => $"int: {n}",
    IEnumerable<int> { } list      => $"list with {list.Count()} items",
    _                              => $"unknown: {result.GetType().Name}"
};

Real-World Example: HTTP Response Handling

record ApiResponse(int StatusCode, string? Body, string? ErrorMessage);

string HandleResponse(ApiResponse response) => response switch
{
    { StatusCode: 200, Body: var body } when body is not null
        => $"Success: {body}",

    { StatusCode: 201, Body: var location }
        => $"Created at: {location ?? "unknown"}",

    { StatusCode: >= 400 and < 500, ErrorMessage: var msg }
        => $"Client error {response.StatusCode}: {msg ?? "no detail"}",

    { StatusCode: >= 500, ErrorMessage: var msg }
        => $"Server error {response.StatusCode}: {msg ?? "internal error"}",

    _   => $"Unexpected status: {response.StatusCode}"
};

Frequently Asked Questions

What is the difference between is and as?
'is' returns true/false and can bind a variable. 'as' returns the cast result or null without throwing. Use 'is' with a variable binding (if obj is string s) over 'as' plus null check — it's cleaner and works for non-nullable value types too.
What are positional patterns?
Positional patterns match against a type's Deconstruct method. If a type has Deconstruct(out int x, out int y), you can match it with (> 0, > 0) and the compiler calls Deconstruct for you. Records get Deconstruct generated automatically.
What are list patterns?
List patterns (C# 11) match against sequences. [1, 2, ..] matches a sequence starting with 1, 2. [_, _, ..rest] matches any sequence with at least 2 elements and captures the remainder. They work on arrays, spans, and any type implementing a Length/Count and indexer.