Pattern Matching in .NET: A Journey from .NET 6 to .NET 9

Introduction

Pattern matching is one of those language features that quietly becomes indispensable: once you’re used to it, you wonder how you’d live without it. In the .NET ecosystem — specifically in C# — pattern matching has matured significantly in recent versions. This post walks through how pattern matching evolved from .NET 6 up through .NET 9, with examples, explanations, and a bit of history to ground things.


A Bit of History: Where Pattern Matching Comes From

To understand how we got here, it’s helpful to look back:

  • Pattern matching as a concept goes way back in computer science — it’s deeply rooted in functional programming languages like ML, Haskell, and others. [1]
  • Early languages such as SNOBOL (1960s) also had pattern matching for strings. [1]
  • In .NET, C# first introduced pattern matching in version 7, but it’s grown a lot since then. ([Microsoft for Developers][2])
  • Over successive C# versions, Microsoft has added more expressive patterns: relational operators, logical combinators (and, or, not), parenthesized patterns, and more. ([codemag.com][3])

Why Pattern Matching Matters

Pattern matching makes your code more expressive and concise:

  • You can match on types, not just use if (obj is SomeType) { … }.
  • You can deconstruct objects (e.g., with switch expressions or property patterns).
  • You can combine conditions very cleanly using logical patterns, making your intent clearer.
  • Using relational patterns (like <, >=) in is or switch avoids boilerplate when clauses.

Pattern Matching in .NET 6 / C# 10 (Baseline)

By the time .NET 6 came around (C# 10), most of the core pattern matching features were already well established:

  • Type patterns: if (obj is MyType t) { … }
  • Constant patterns: case 42: … or if (x is 42) …
  • Property patterns: e.g., if (p is Point { X: 0, Y: 0 }) …
  • Positional patterns (with deconstruct): if (p is Point(var x, var y)) …
  • Switch expressions using patterns.

Here’s a small example in C# 10 / .NET 6:





How it works:

public record Point(int X, int Y);

object GetSomething() => new Point(3, 4);

void Process()
{
    object o = GetSomething();

    if (o is Point { X: 0, Y: 0 })
    {
        Console.WriteLine("Origin");
    }
    else if (o is Point(var x, var y))
    {
        Console.WriteLine($"Point with coordinates ({x}, {y})");
    }
    else
    {
        Console.WriteLine("Not a Point");
    }

    // Using switch expression:
    string description = o switch
    {
        Point { X: 0, Y: 0 } => "At origin",
        Point(var x, var y) => $"At ({x}, {y})",
        _ => "Unknown object"
    };

    Console.WriteLine(description);
}


  • if (o is Point { X: 0, Y: 0 }): Checks if o is a Point and destructures it, matching its properties.
  • Point(var x, var y): Uses deconstruction (positional) pattern.
  • The switch expression is very readable: pattern on left, result on right.

While the major pattern matching leaps came earlier, many of the improvements up to .NET 7 / C# 11 were incremental (optimization, compiler refinement) rather than huge syntactic breakthroughs. As such, most blog posts emphasize changes in C# 8 and C# 9 for pattern matching. There were no widely-advertised brand-new pattern constructs introduced in .NET 7 specifically for pattern matching.


Big Leap: Pattern Matching in .NET 8 / C# 12 (Preview of what’s coming in .NET 9)

Although your question goes up to .NET 9, it’s the C# language version that drives much of the pattern-matching syntax, so it’s useful to talk about what’s new or expected around C# 12 / .NET 9.

According to community and preview sources: ([DEV Community][4])

  • Enhanced pattern matching: There’s continuing work to make pattern expressions more concise and expressive, particularly logical and relational patterns.
  • Though some sources talk about C# 12 (which aligns with .NET 9) introducing more pattern matching improvements, it’s not always clearly documented in official release notes — because features may still be in preview or evolving in the language design process.

Deep Dive: What C# 9 Introduced (Pattern Matching Enhancements)

Most of the big pattern-matching improvements came in C# 9, released with .NET 5, but still very relevant for .NET 6–9, since C# 9 features continued to be supported.

Here’s a breakdown:

1. Relational Patterns

You can now write patterns using <, >, <=, >= directly.

int age = 25;

string ageGroup = age switch
{
    < 13 => "Child",
    < 20 => "Teenager",
    < 65 => "Adult",
    _ => "Senior"
};

Console.WriteLine(ageGroup);  // Output: "Adult"

Explanation:
Instead of using when clauses, the relational operators are part of the pattern, making it concise and expressive. ([Microsoft for Developers][2])


2. Logical Patterns: and, or, not

These let you combine patterns in more English-like ways.

char c = 'G';

bool IsLetter = c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
Console.WriteLine(IsLetter);  // True

string? maybe = null;
if (maybe is not null)
{
    Console.WriteLine($"You said: {maybe}");
}
else
{
    Console.WriteLine("Nothing typed");
}

Explanation:

  • >= 'a' and <= 'z' is a conjunctive (AND) pattern: both conditions must hold.
  • or lets you express alternate shapes (either lowercase or uppercase letter).
  • not provides a simpler, more readable null check (is not null) vs. != null. ([anthonygiretti.com][5])

3. Parenthesized Patterns

Parentheses help clarify grouping, especially when combining logical and relational patterns.

if (c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z'))
{
    Console.WriteLine("It's a letter!");
}

Here the parentheses make it clear which relational checks are grouped together.


4. Negated (not) Pattern

As shown above, not is handy.

object? obj = new object();
if (obj is not null)
{
    Console.WriteLine("Got non-null object");
}

This is effectively a more pattern-based way to say obj != null. ([Telerik.com][6])


5. Combining Patterns in is and switch

You can use these new patterns both in is expressions and switch statements / expressions.

object? value = 100;

string description = value switch
{
    int i and > 0 => "Positive integer",
    int i and <= 0 => "Zero or negative integer",
    string s => $"String of length {s.Length}",
    null => "Null value",
    _ => "Other type"
};

Console.WriteLine(description);

How this works:

  • int i and > 0: first checks if value is an int (type pattern), captures it in i, then also checks the relational condition that i > 0.
  • null is matched explicitly.
  • _ is the discard pattern: anything else.

Putting It All Together: A Mini Real-World Example

Let’s build a mini console app that classifies messages:

public record Message(string Sender, string Content, DateTime Timestamp);

string ClassifyMessage(object? msg)
{
    return msg switch
    {
        Message { Sender: "System", Content: "Ping", Timestamp: var ts } when ts < DateTime.UtcNow.AddMinutes(-5)
            => "Stale system ping",
        Message { Sender: "System", Content: "Ping", Timestamp: _ }
            => "Recent system ping",
        Message { Sender: var s and not "", Content: var c } => $"User '{s}' said: {c}",
        string s when s.Length == 0 => "Empty user message",
        null => "No message",
        _ => "Unknown object"
    };
}

// Usage:
var m1 = new Message("Alice", "Hello", DateTime.UtcNow);
var m2 = "Just a random string";
var m3 = (Message?)null;

Console.WriteLine(ClassifyMessage(m1));  
Console.WriteLine(ClassifyMessage(m2));  
Console.WriteLine(ClassifyMessage(m3));

Explanation:

  • We match on a Message record, using a property pattern (Sender, Content, Timestamp).
  • The first case even combines a when guard for a “stale” timestamp.
  • We use and not to ensure Sender is not empty.
  • We match plain strings, nulls, and anything else.

What About .NET 9 and Beyond?

As of the latest previews and community writing:

  • .NET 9 (and upcoming C# 12 in some sources) seems to continue enhancing pattern matching. One blog notes “enhancements in pattern matching provide more concise and expressive coding capabilities.” ([DEV Community][4])
  • There’s interest in even more expressive patterns, possibly more advanced “type expressions” within switch statements. ([ByteHide][7])
  • That said, some features remain in preview, and full official documentation may lag behind blog/community discussions.

Tips for Developers (Junior → Senior)

  • Junior developers: Start by using type patterns, switch expressions, and simple property patterns. These make code cleaner and more readable, especially when dealing with polymorphic data.
  • Intermediate developers: Explore relational and logical patterns (and, or, not). They help you reduce boilerplate (no more long if-else chains).
  • Senior developers: Use advanced combinations, parenthesized patterns, and even guard clauses (when) in switch expressions to build expressive, maintainable control flow. Also, think about pattern matching for domain-driven design: matching “shapes” of data is often more robust than fragile if chains.

References & Further Reading

Leave a Reply