Skip to content

.NET Core · C# Core

C# Pattern Matching and Switch Expressions

7 min read Updated 2026-06-23 Share:

Practice Pattern Matching interview questions

Why pattern matching keeps getting better with each C# version

Every major C# release since C# 7 has expanded pattern matching. Interviewers test it because it reflects modern idiomatic C# — candidates who know only C# 6 write verbose, multi-line type-checking code that modern C# collapses to a single expression. This guide covers every pattern type and when to reach for each.

The problem pattern matching solves

Pre-C# 7 type-dispatching code:

// Old style — verbose and error-prone:
object value = GetValue();
if (value is int)
{
    int n = (int)value;
    Console.WriteLine(n * 2);
}
else if (value is string)
{
    string s = (string)value;
    Console.WriteLine(s.ToUpper());
}

Modern C# 7+:

// Type pattern — test and bind in one step:
if (value is int n)
    Console.WriteLine(n * 2);
else if (value is string s)
    Console.WriteLine(s.ToUpper());

// Better: switch expression (C# 8):
string result = value switch
{
    int n    => $"int: {n * 2}",
    string s => s.ToUpper(),
    null     => "null",
    _        => value.GetType().Name
};

switch expression vs switch statement

The switch statement executes side effects. The switch expression produces a value — it is an expression, not a statement.

// switch statement (C# 1+): imperative, branches, requires break
string label;
switch (status)
{
    case OrderStatus.New:      label = "New";      break;
    case OrderStatus.Shipped:  label = "Shipped";  break;
    case OrderStatus.Delivered: label = "Delivered"; break;
    default:                   label = "Unknown";  break;
}

// switch expression (C# 8+): expression, no break, exhaustiveness checked by compiler
string label2 = status switch
{
    OrderStatus.New       => "New",
    OrderStatus.Shipped   => "Shipped",
    OrderStatus.Delivered => "Delivered",
    _                     => "Unknown",
};

The compiler warns if a switch expression might not cover all inputs (it is not exhaustive). Omitting the discard _ arm when values are possible means a SwitchExpressionException at runtime.

Type patterns

A type pattern tests the runtime type and binds the value:

Shape shape = GetShape();

// Type pattern in switch expression:
double area = shape switch
{
    Circle c    => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.Width * r.Height,
    Triangle t  => 0.5 * t.Base * t.Height,
    _           => throw new ArgumentException($"Unknown shape: {shape.GetType().Name}")
};

// Inheritance: tests runtime type, not compile-time type
object obj = new Dog();
if (obj is Animal a)          // matches — Dog IS-A Animal
    Console.WriteLine(a.GetType().Name); // "Dog"

Type patterns never match null — if shape is null, none of the arms match and the discard _ catches it.

Property patterns

Property patterns match objects by the value of their properties:

record Address(string Country, string PostCode);
record Customer(string Name, Address Address, bool IsPremium, decimal Balance);

Customer c = new("Alice", new Address("UK", "EC1A"), true, 500m);

string offer = c switch
{
    // Nested property pattern (C# 10 dot notation):
    { IsPremium: true, Address.Country: "UK", Balance: > 1000 } => "Gold UK member",
    { IsPremium: true, Address.Country: "UK" }                   => "Premium UK",
    { IsPremium: true }                                           => "Premium",
    { Address.Country: "UK" }                                     => "Standard UK",
    _                                                             => "Standard"
};
Console.WriteLine(offer); // Premium UK

C# 10 introduced extended property patterns: { Address.Country: "UK" } instead of the verbose C# 8 form { Address: { Country: "UK" } }.

Combine property patterns with type patterns:

object obj = new Customer("Bob", new Address("US", "NY"), false, 0m);

if (obj is Customer { Name: var name, Balance: 0, IsPremium: false })
    Console.WriteLine($"Inactive customer: {name}");

Positional patterns

Positional patterns use a type's Deconstruct method. Records get deconstruction for free:

record Point(int X, int Y);

Point p = new(3, -5);

string quadrant = p switch
{
    (0, 0)     => "Origin",
    (> 0, > 0) => "Q1",
    (< 0, > 0) => "Q2",
    (< 0, < 0) => "Q3",
    (> 0, < 0) => "Q4",
    _           => "On axis"
};
Console.WriteLine(quadrant); // Q4

// Tuples also support positional patterns:
(string role, bool isActive) user = ("admin", false);
string access = user switch
{
    ("admin", true)  => "Full access",
    ("admin", false) => "Account suspended",
    (_, true)        => "Limited access",
    _                => "No access"
};

Relational and logical patterns (C# 9)

Relational patterns compare with <, <=, >, >=. Logical patterns combine with and, or, not:

int temperature = 72;

string weather = temperature switch
{
    < 0           => "Freezing",
    >= 0 and < 10 => "Cold",
    >= 10 and < 20 => "Cool",
    >= 20 and < 30 => "Comfortable",
    >= 30 and < 40 => "Hot",
    _             => "Extreme heat"
};

// Logical 'or':
DayOfWeek day = DateTime.Today.DayOfWeek;
bool isWeekend = day is DayOfWeek.Saturday or DayOfWeek.Sunday;

// Logical 'not':
string? name = GetName();
if (name is not null and { Length: > 0 })
    Console.WriteLine(name.ToUpper());

Relational patterns only work with comparable value types (numeric types, char).

when guards

when adds an arbitrary boolean condition to a pattern arm. It runs only if the pattern already matched:

// Use when for logic that can't be expressed as a pure pattern:
static string ClassifyOrder(Order order) => order switch
{
    { Status: OrderStatus.Shipped } when order.DaysInTransit > 14 => "Potentially lost",
    { Status: OrderStatus.Shipped }                                 => "In transit",
    { Total: var t } when t > 10_000                               => "High-value order",
    { IsPaid: false, Total: > 0 }                                  => "Payment pending",
    _                                                               => "Standard"
};

The when filter runs after the pattern is tested, so the matched variable is available in the guard expression.

List patterns (C# 11)

List patterns match arrays and lists by structure:

int[] data = { 1, 2, 3, 4, 5 };

// Exact match:
bool exact = data is [1, 2, 3, 4, 5]; // true

// Partial match with slice:
bool starts = data is [1, 2, ..];     // true — starts with 1, 2
bool ends   = data is [.., 4, 5];     // true — ends with 4, 5
bool middle = data is [_, _, 3, ..];  // true — third element is 3

// Capture with var:
if (data is [var head, .. var rest])
    Console.WriteLine($"head={head}, rest has {rest.Length} elements"); // head=1, rest has 4

// Routing example:
string[] segments = { "api", "products", "42" };
string response = segments switch
{
    ["api", "products", var id] => $"Get product {id}",
    ["api", "products"]         => "List products",
    ["api", ..]                 => "Other API endpoint",
    _                           => "Not found"
};
Console.WriteLine(response); // Get product 42

Deconstruction and the var pattern

Deconstruction unpacks a type into components. The var pattern binds any value without type-checking (always succeeds, including null):

// Deconstruction in assignment:
var (x, y) = new Point(3, 7);
var (name, age) = ("Alice", 30);

// var pattern — bind for use in when guard:
object obj = "hello world";
string desc = obj switch
{
    null          => "null",
    var s when s is string str && str.Contains(' ') => $"sentence: {str}",
    var s         => $"other: {s}"
};

// Discard in deconstruction:
var (_, height) = new Rectangle(10, 5); // only care about height

// foreach deconstruction:
var people = new[] { ("Alice", 30), ("Bob", 25) };
foreach (var (n, a) in people)
    Console.WriteLine($"{n} is {a}");

Null safety in patterns

Type patterns are null-safe by design — null never matches a type pattern:

object? value = null;

if (value is string s) // false — null is not a string; no NullReferenceException
    Console.WriteLine(s);

// Prefer 'is null' and 'is not null' over == null:
string? name = GetName();
if (name is not null and { Length: > 0 }) // short-circuit: null check first
    Console.WriteLine(name.ToUpper());

// Property pattern on nullable:
Address? addr = GetAddress();
if (addr is { Country: "UK", PostCode.Length: > 0 }) // safe — false if addr is null
    Console.WriteLine("Valid UK address");

Recap

Pattern matching in C# has evolved from simple is-checks into a powerful expression- based dispatch system. Switch expressions are value-producing, exhaustive-checked alternatives to switch statements. Type patterns test and bind runtime types, safely handling null. Property patterns dispatch on object shape, making business-rule code declarative and readable. Positional patterns work with records and deconstruction. Relational and logical patterns (and, or, not, <, >=) express range and exclusion conditions without verbose when guards. List patterns (C# 11) match sequences by structure. when guards add arbitrary conditions when pure patterns aren't enough. The var pattern captures any value for use in a guard expression. In all cases, type patterns never match null — use explicit null or not null patterns for null handling.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel