Skip to content

Pattern Matching

Pattern matching lets you ask richer questions than simple equality or boolean conditions. Instead of asking only “is this true or false?”, you can ask things like:

  • Is this value an int?
  • Is it greater than 0?
  • Does this object have certain property values?
  • Does this sequence start with a particular shape?

This makes code more expressive because the condition can describe type, value, range, and sometimes structure in one place.

Why pattern matching matters

Without pattern matching, you often need several separate steps:

  1. check a type
  2. cast the value
  3. inspect the casted value

Pattern matching combines these ideas into one readable construct.

csharp
object value = 10;

if (value is int number && number > 5)
{
    Console.WriteLine($"Large integer: {number}");
}

That single is pattern both checks the runtime type and introduces a typed variable named number.

Pattern types at a glance

mermaid
flowchart TD
    P["Pattern Matching"] --> T["Type patterns\nis int number"]
    P --> C["Constant patterns\nis null, is 0, case \"yes\""]
    P --> R["Relational patterns\n> 0, <= 100"]
    P --> L["Logical patterns\nand, or, not"]
    P --> PR["Property patterns\n{ IsActive: true }"]
    P --> LP["List patterns\n[1, 2, ..]"]

You do not need every pattern form at once. Start with type, constant, relational, and logical patterns. Those are the most immediately useful.

Type patterns

Type patterns check whether a value is a specific runtime type.

csharp
object input = "hello";

if (input is string text)
{
    Console.WriteLine(text.ToUpper());
}

If the runtime object really is a string, the pattern succeeds and text becomes available inside the block.

This is usually clearer and safer than doing a direct cast first.

Constant patterns

Constant patterns compare against a fixed value.

csharp
string? command = null;

if (command is null)
{
    Console.WriteLine("No command provided.");
}

You will often see constant patterns with null, numbers, strings, and enum values.

Relational and logical patterns

Relational patterns let you check ranges. Logical patterns let you combine checks.

csharp
int temperature = 22;

if (temperature is >= 20 and <= 25)
{
    Console.WriteLine("Comfortable temperature");
}

This reads almost like a sentence, which is one reason modern pattern matching can improve readability.

Property patterns

Property patterns inspect object members directly inside the pattern.

csharp
var order = new { Total = 120m, IsPaid = true };

if (order is { Total: >= 100m, IsPaid: true })
{
    Console.WriteLine("Large paid order");
}

This is useful when you care about the shape of an object more than the object type alone.

Pattern matching in switch

Patterns become especially powerful inside switch expressions or statements.

csharp
object input = 15;

string description = input switch
{
    null => "No value",
    int number when number < 0 => "Negative integer",
    int number => $"Integer: {number}",
    string text => $"Text with length {text.Length}",
    _ => "Unknown value"
};

This is far richer than a basic switch on exact constant values.

A useful mental model

Think of pattern matching as asking whether a value fits a description.

The description might be:

  • a type
  • an exact value
  • a numeric range
  • a logical combination of rules
  • an object with certain properties

That is why pattern matching belongs in the flow-control chapter. It is decision logic, but more expressive than plain boolean comparisons.

A worked example

Suppose a program receives mixed input from a command pipeline and wants to describe it safely.

csharp
static string DescribeInput(object? input) => input switch
{
    null => "No value supplied",
    int number and < 0 => "Negative whole number",
    int number => $"Whole number: {number}",
    decimal amount and >= 100m => $"Large amount: {amount:C}",
    decimal amount => $"Amount: {amount:C}",
    string { Length: 0 } => "Empty text",
    string text => $"Text: {text}",
    _ => "Unsupported input type"
};

This example shows several important things at once:

  • null is handled explicitly
  • integer and decimal values can take different branches
  • ranges can refine a type pattern
  • strings can be matched by a property such as Length
  • the discard pattern _ acts as a fallback

This is often easier to read than a long sequence of casts and nested if statements.

Common mistakes

  • Using a direct cast when a type pattern would be safer.
  • Writing patterns in an order where broader matches come before more specific ones.
  • Forgetting that pattern variables exist only where the pattern is known to have matched.
  • Introducing complex patterns without making the intent clearer.

Pattern matching is powerful, but it should still improve readability rather than turn conditions into puzzles.

Summary

Pattern matching lets C# express decision logic in a more descriptive way.

The core beginner-friendly forms are:

  • type patterns
  • constant patterns
  • relational patterns
  • logical patterns
  • property patterns

When used well, patterns reduce manual casts, make intent clearer, and keep condition logic closer to the shape of the data being tested.

Practice

Write an if statement that uses a type pattern to check whether an object contains a string, and if it does, print the string length.

As a second exercise, write a switch expression that classifies an int as negative, zero, small positive, or large positive using relational patterns.

Built with VitePress and published on GitHub Pages.