Skip to main content
SEMastery
.NET Corebeginner

How to Write Elegant Code with C# Pattern Matching

Learn C# pattern matching the easy way. Use is, switch expressions, property and list patterns to write clean, readable, and elegant .NET code.

11 min readUpdated May 26, 2026

Imagine you are sorting the mail that arrives at your house. You look at each envelope. Is it a bill? Put it in one pile. Is it a letter from a friend? Another pile. Is it junk? Straight to the bin. You do not read every word. You just look at the shape and the markings and decide quickly.

That is exactly what pattern matching does in C#. You hand the computer a piece of data, and it checks the shape and the value of that data, then picks the right thing to do. No long chains of confusing if and else. Just clean, calm, readable code.

In this guide we will learn pattern matching step by step, in plain English, with small examples you can try yourself. By the end, your if ladders will turn into short, elegant lines.

What problem are we solving?

Let us look at some old-style code first. Say we want to print a friendly message based on a number score.

string GetGrade(int score)
{
    if (score >= 90)
        return "Excellent";
    else if (score >= 70)
        return "Good";
    else if (score >= 40)
        return "Pass";
    else
        return "Try again";
}

This works, but it is a bit noisy. There is a lot of if, else if, and return repeated again and again. When you add more cases, it grows messy. Pattern matching gives us a cleaner way to write the same idea.

The old if-else ladder forces the reader to walk down every branch.

The is keyword: your first pattern

The simplest pattern uses the is keyword. It asks one question: does this data match this shape?

A very common job is checking the type of an object. In the old days you had to cast first, then check for null. Now you can do it in one step.

object box = "hello world";
 
// Old way
string text = box as string;
if (text != null)
{
    Console.WriteLine(text.ToUpper());
}
 
// Pattern matching way
if (box is string message)
{
    Console.WriteLine(message.ToUpper());
}

Read the second one out loud: "if box is a string, call it message." If the test passes, you instantly get a new variable named message that is already the right type and is never null. This is called a declaration pattern. It checks the type and gives you a ready-to-use variable at the same time.

Checking for null and not-null

You can also use patterns to check for null in a clear way.

if (user is null)
    return "No user found";
 
if (user is not null)
    Console.WriteLine(user.Name);

The words is null and is not null read like normal English. They are also safer than == because they cannot be tricked by a class that overrides the equals operator.

The switch expression: the real star

Here is where code becomes truly elegant. The switch expression lets you map an input to a result in a compact block. Let us rewrite our grade example.

string GetGrade(int score) => score switch
{
    >= 90 => "Excellent",
    >= 70 => "Good",
    >= 40 => "Pass",
    _     => "Try again"
};

Look how short that is. Each line says "this pattern, then this result." The little _ at the bottom is the discard pattern. It means "anything else," like the else at the end of an if ladder. It also keeps your switch safe, because the compiler wants every possible value to be handled.

Those >= 90 and >= 70 parts are relational patterns. They compare your value against a constant using <, >, <=, or >=. The arms are checked from top to bottom, so order matters: put the most specific case first.

A switch expression takes one input and returns one matched result.

Combining patterns with and, or, not

You can join patterns together with logical words. This is called a logical pattern. It makes ranges and combos very easy to read.

string DescribeTemp(int celsius) => celsius switch
{
    < 0           => "Freezing",
    >= 0 and < 15 => "Cold",
    >= 15 and < 30 => "Pleasant",
    >= 30 and < 45 => "Hot",
    _             => "Dangerously hot"
};

The line >= 15 and < 30 reads almost like a sentence. Compare that to writing celsius >= 15 && celsius < 30 everywhere. The pattern version is calmer and harder to get wrong.

Property patterns: looking inside objects

Sometimes you do not care about the type of an object. You care about its insides, its properties. A property pattern lets you peek inside and match on the fields you care about.

Imagine a small Order record.

record Order(string Country, decimal Amount, bool IsGift);
 
decimal CalculateShipping(Order order) => order switch
{
    { Country: "IN", Amount: >= 500 }    => 0m,      // free shipping
    { Country: "IN" }                    => 40m,
    { IsGift: true, Amount: < 1000 }     => 60m,
    _                                    => 100m
};

Read the first arm: "if the order's Country is IN and the Amount is 500 or more, shipping is free." You match several properties at once, inside curly braces, and you can even use relational patterns like >= 500 on them. This removes whole stacks of nested if statements.

You can also reach into nested objects. If Order had a Customer with a City, you could write { Customer.City: "Mumbai" } to match deep inside.

How a property pattern is checked

Receive object
Check property 1
Check property 2
Match or skip

Steps

1

Receive object

Switch gets the order

2

Check property 1

Country == IN?

3

Check property 2

Amount >= 500?

4

Match or skip

All pass: run this arm

The compiler checks each named property in turn; if any fails, the arm is skipped.

A quick comparison of the pattern kinds

Here is a small table so you can see the main patterns side by side. Keep this nearby while you practice.

PatternWhat it checksExample
Type / declarationIs it this type?obj is string s
ConstantEquals a fixed valuex is 0
RelationalCompares with <, >, etc.x is >= 18
LogicalJoins with and, or, notx is > 0 and < 10
PropertyMatches object fields{ Age: >= 18 }
ListMatches a sequence shape[1, 2, ..]

And here is when to reach for each main tool.

ToolBest forReturns a value?
is expressionA single quick checkNo, gives true/false
switch statementMany actions, no result neededNo
switch expressionMapping input to one outputYes

List patterns: matching sequences

Since C# 11 you can match on the shape of a list or array. This is called a list pattern, and it feels almost magical the first time you use it.

int[] numbers = { 1, 2, 3 };
 
string Describe(int[] values) => values switch
{
    []        => "Empty",
    [var only] => $"One item: {only}",
    [var first, .., var last] => $"Starts {first}, ends {last}",
    _         => "Something else"
};

The [] matches an empty list. The [var only] matches a list with exactly one item and names it only. The .. is a slice pattern, meaning "any number of items here, I do not care." So [var first, .., var last] grabs the first and last items and ignores the middle. This is wonderful for parsing commands or small data shapes.

List patterns test the length and the position of items in a sequence.

A real example: handling shapes

Let us put several patterns together in one tidy method. Say we have different shapes and we want their area.

abstract record Shape;
record Circle(double Radius) : Shape;
record Rectangle(double Width, double Height) : Shape;
record Square(double Side) : Shape;
 
double Area(Shape shape) => shape switch
{
    Circle { Radius: var r }            => Math.PI * r * r,
    Rectangle { Width: var w, Height: var h } => w * h,
    Square s                            => s.Side * s.Side,
    null                                => throw new ArgumentNullException(nameof(shape)),
    _                                   => throw new ArgumentException("Unknown shape")
};

This single method handles every shape clearly. Each arm matches a type and pulls out exactly the values it needs. If someone later adds a new shape and forgets a case, the _ arm protects the program from crashing in a confusing way. This is the heart of writing elegant code: each idea sits on its own line, and the reader understands it at a glance.

Shape area decision

Shape in
Match type
Extract values
Return area

Steps

1

Shape in

Circle, Rect, or Square

2

Match type

Pick the right arm

3

Extract values

Get radius or sides

4

Return area

Compute and return

One switch routes each shape type to its own formula.

Tips for clean, safe patterns

A few small habits keep your pattern code friendly:

  • Order from specific to general. The switch checks arms top to bottom. Put the narrow cases first and the broad _ last.
  • Always include a catch-all with _ in a switch expression, unless the compiler can already prove every case is handled. This avoids surprise exceptions.
  • Do not over-pack one arm. If a pattern grows huge, it is a sign to split it or pull a helper method.
  • Use var to name parts you want to use, and _ for parts you want to ignore.
  • Prefer is null over == null for safety.

One more note for teams: patterns are not slow. The compiler turns them into the same fast comparisons you would write by hand, and list and span patterns can even avoid extra memory work. So you get clean code and good speed.

What is new and what to remember

Pattern matching has grown a lot over the years. Here is the short history so you know what each version brought:

C# versionAdded
C# 7is type patterns, basic switch
C# 8switch expression, property patterns
C# 9relational and logical patterns
C# 11list and slice patterns
C# 14uses all of the above, no breaking changes

C# 14 ships with .NET 10, which is the current Long Term Support (LTS) release. Pattern matching did not change in C# 12, 13, or 14, so everything you learned here works on the latest .NET today. Looking ahead, C# 15 in the .NET 11 preview is exploring union types, which will pair even more nicely with switch expressions in the future.

Quick recap

  • Pattern matching checks the shape, type, and value of data, then acts on it, like sorting mail by its markings.
  • The is keyword does quick checks and can give you a typed variable in one step.
  • The switch expression maps an input to one clean result, with _ as the catch-all.
  • Relational (>= 90) and logical (and, or, not) patterns make ranges easy to read.
  • Property patterns peek inside objects, and can reach into nested fields.
  • List patterns match the shape of arrays and sequences, with .. for "any items here."
  • Order arms from specific to general, always handle the leftover case, and keep each arm small.
  • It is fast: the compiler turns patterns into efficient code, so you get elegance without cost.

References and further reading

Related Posts