C#

C# Fundamentals

19 lessons

Progress0%
1. Introduction to C#
1What is C#?
2. Variables and Data Types
1Data Types in C#
3. Control Flow
ConditionalsLoops
4. Methods
Defining MethodsOptional Parameters and Overloading
5. Object-Oriented Programming
Classes and PropertiesInheritanceInterfaces and Generics
6. LINQ and Async
LINQ Queriesasync/await
7. Exception Handling
try/catch/finally & Exception TypesCustom Exceptions & IDisposable
8. Delegates & Events
Delegates & LambdaEvents & Event Handlers
9. Records & Pattern Matching
Record TypesPattern Matching & Switch Expressions
10. File I/O & JSON
File & Stream OperationsJSON Serialization
All Tutorials
C#Records & Pattern Matching
Lesson 17 of 19 min
Chapter 9 · Lesson 2

Pattern Matching & Switch Expressions

Pattern matching lets you test a value against a shape and extract information from it in a single, concise expression. C# has grown a rich set of patterns since C# 7, with the most powerful additions arriving in C# 8–11.

is Pattern

The simplest form checks type and binds a variable in one step:

code
if (obj is string s)
    Console.WriteLine(s.Length);

Switch Expression (C# 8+)

The switch expression (as opposed to the switch statement) evaluates to a value and is exhaustive — the compiler warns if cases are not covered. Arms use => instead of case/break:

code
string label = shape switch
{
    Circle c  => ````````circle r=${c.Radius}````````,
    Rectangle r => ````````rect ${r.W}×${r.H}````````,
    _ => "unknown"
};

Property Patterns

Match on the values of properties without needing a variable:

code
if (order is { Status: OrderStatus.Shipped, Total: > 1000 })
    ApplyDiscount(order);

Positional Patterns

Work with types that have a Deconstruct method (including tuples and records):

code
var point = new Point(1, -1);
if (point is (> 0, < 0))
    Console.WriteLine("Quadrant IV");

List Patterns (C# 11+)

Match arrays or lists by their element structure:

code
int[] arr = { 1, 2, 3 };
if (arr is [1, .., 3]) // starts with 1, ends with 3
    Console.WriteLine("Matched");

when Guards

Add extra conditions to any pattern arm:

code
shape switch
{
    Circle c when c.Radius > 100 => "huge circle",
    Circle c                     => "small circle",
}

Type Patterns and Recursive Patterns

You can nest patterns: a type pattern inside a property pattern, or a property pattern inside a positional pattern. This allows deeply structured matching in a single expression, replacing chains of if/else if with is checks.

Code Examples

Switch Expression with Type Patternscsharp
using System;

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

static double Area(Shape shape) => shape switch
{
    Circle c      => Math.PI * c.Radius * c.Radius,
    Rectangle r   => r.Width * r.Height,
    Triangle t    => 0.5 * t.Base * t.Height,
    null          => throw new ArgumentNullException(nameof(shape)),
    _             => throw new NotSupportedException(````````Unknown shape: ${shape.GetType().Name}````````),
};

static string Classify(Shape shape) => shape switch
{
    Circle c when c.Radius > 10  => "Large circle",
    Circle c when c.Radius > 5   => "Medium circle",
    Circle                       => "Small circle",
    Rectangle r when r.Width == r.Height => "Square",
    Rectangle                    => "Rectangle",
    _                            => "Other shape",
};

Shape[] shapes = { new Circle(3), new Circle(7), new Circle(15),
                   new Rectangle(4, 4), new Rectangle(5, 3), new Triangle(6, 4) };

foreach (var s in shapes)
{
    Console.WriteLine(````````${Classify(s),-18} area = ${Area(s):F2}````````);
}

Switch expressions evaluate top-to-bottom and return a value. 'when' guards refine type matches. The discard '_' arm acts as the default. The compiler verifies exhaustiveness.

Property Pattern Matchingcsharp
using System;

public record Order(int Id, string Status, decimal Total, string Country);

static string ClassifyOrder(Order order) => order switch
{
    // Property pattern — match on property values
    { Status: "Cancelled" }                              => "Cancelled order",
    { Status: "Shipped", Total: > 500, Country: "US" }  => "High-value US shipment",
    { Status: "Shipped", Total: > 500 }                  => "High-value international shipment",
    { Status: "Shipped" }                                => "Standard shipment",
    { Status: "Pending", Total: 0 }                      => "Empty pending order",
    { Status: "Pending" }                                => "Pending order",
    _                                                    => "Unknown status",
};

var orders = new[]
{
    new Order(1, "Pending",   0m,    "UK"),
    new Order(2, "Pending",   120m,  "UK"),
    new Order(3, "Shipped",   650m,  "US"),
    new Order(4, "Shipped",   650m,  "DE"),
    new Order(5, "Shipped",   80m,   "FR"),
    new Order(6, "Cancelled", 200m,  "US"),
};

foreach (var o in orders)
    Console.WriteLine(````````Order ${o.Id}: ${ClassifyOrder(o)}````````);

Property patterns match multiple property values simultaneously using { Property: pattern } syntax. More specific arms should come before general ones, just like catch blocks.

Nested / Recursive Pattern Examplecsharp
using System;

record Address(string City, string Country);
record Customer(string Name, Address Address, decimal CreditLimit);

static string RouteCustomer(Customer customer) => customer switch
{
    // Nested property pattern — drill into Address from within Customer pattern
    { Address: { Country: "US", City: "New York" }, CreditLimit: > 10_000 }
        => "Premium NYC client → dedicated manager",

    { Address: { Country: "US" }, CreditLimit: > 5_000 }
        => "High-value US client → express support",

    { Address: { Country: "US" } }
        => "Standard US client → self-service",

    { Address: { Country: var c }, CreditLimit: > 5_000 }
        => ````````High-value client in ${c} → international team````````,

    { Address: { Country: var c } }
        => ````````Standard client in ${c} → regional support````````,
};

// List pattern (C# 11+)
static string DescribeList(int[] data) => data switch
{
    []           => "Empty list",
    [var single] => ````````Single element: ${single}````````,
    [var first, .., var last] => ````````${data.Length} elements, first=${first}, last=${last}````````,
};

var customers = new[]
{
    new Customer("Alice", new Address("New York", "US"), 15_000m),
    new Customer("Bob",   new Address("Chicago",  "US"),  6_000m),
    new Customer("Carol", new Address("London",   "UK"),  8_000m),
    new Customer("Dave",  new Address("Berlin",   "DE"),  2_000m),
};

foreach (var c in customers)
    Console.WriteLine(````````${c.Name}: ${RouteCustomer(c)}````````);

Console.WriteLine();
Console.WriteLine(DescribeList(Array.Empty<int>()));
Console.WriteLine(DescribeList(new[] { 42 }));
Console.WriteLine(DescribeList(new[] { 1, 2, 3, 4, 5 }));

Nested property patterns allow matching deep object graphs in one expression. The 'var' pattern binds a value inside a pattern for use in the arm. List patterns (C# 11) match arrays by position and can use '..' to skip middle elements.

Quick Quiz

1. What is the key syntactic difference between a switch statement and a switch expression in C#?

2. What does a property pattern like '{ Status: "Shipped", Total: > 100 }' check?

3. What does the list pattern '[var first, .., var last]' match?

Was this lesson helpful?

PreviousNext