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
switchexpressions or property patterns). - You can combine conditions very cleanly using logical patterns, making your intent clearer.
- Using relational patterns (like
<,>=) inisorswitchavoids boilerplatewhenclauses.
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: …orif (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 ifois aPointand 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.
New in .NET 7 / C# 11 — (if any pattern-related changes)
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.orlets you express alternate shapes (either lowercase or uppercase letter).notprovides 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 ifvalueis anint(type pattern), captures it ini, then also checks the relational condition thati > 0.nullis 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
Messagerecord, using a property pattern (Sender,Content,Timestamp). - The first case even combines a
whenguard for a “stale” timestamp. - We use
and notto ensureSenderis 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
switchstatements. ([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 longif-elsechains). - 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 fragileifchains.
References & Further Reading
- History of pattern matching: [1]
- New pattern matching in C# 9
- Explanation of combinator patterns: ([NDepend Blog][9])
- Preview features in .NET 9 / C# 12: ([DEV Community][4])
