NDepend Blog

Improve your .NET code quality with NDepend

C# Pattern Matching Explained (2025)

June 17, 2025 7 minutes read

 C#-Pattern-Matching-Explained

Pattern matching in C# involves assessing an expression to see if it fits one or several particular condition—like checking if it’s of a specific type or equals a certain constant.With pattern matching, complex if-else logic becomes more compact and readable, avoiding repetition of the expression—like person in the code sample below—in each condition.

The most effective way to explain pattern matching is to trace its development from its introduction in C# 7.0, highlighting how it has evolved.  The present post will be updated with future C# pattern matching evolutions.

C#7: Null, Constant, Type, Discard and Var Patterns

C#7 introduced checking against a null pattern and a constant pattern.

One limitation of pattern matching, even in the latest C# versions, is that the values embedded within a pattern must be a compile-time constant. In many cases, the values to be matched against are not hardcoded in C# code but instead retrieved from configurations. This restriction limits the applicability of pattern matching in several real-world scenarios.

Pattern-Matching-Requires-Constant

Notice that constant supported by patterns are:

  • an integer or floating-point numerical literal
  • a char or a string literal
  • a boolean value true or false
  • an enumeration value
  • the name of a declared const field or null

C#7 also introduced the type pattern, which is a great improvement, especially to introduce temporary variables in complex bool expressions:

Pattern matching works also with switch statements. This is especially useful to manage control flows through types that aren’t related by an inheritance hierarchy.

Notice the last default: clause that matches when seq is null. This is the discard pattern.

Notice also that the order of the case clauses matters. For example, since the class List<T> implements the interface ICollection<T>, the compiler is smart enough to prevent such mistake:

Pattern-Matching-Clause-Order-Matters

Also with switch statement the keyword when can be used to add additional conditions that refine the pattern:

Let’s underline that the keyword when cannot be used in if statement -only in switch statement – even with the latest C# versions:

Pattern-Matching-when-cannot-be-used-in-if-expression

Finally C# 7 introduced the var pattern which is a special type pattern that matches even when null.

It is not recommended to use the var pattern to skip null check because an undefined null state is dangerous. Practically the var pattern is used in complex situations where anonymous types are involved.

C# 8: Switch Expressions and Property, Positional and Tuple Patterns

C#8 improved pattern matching in several ways. First, there is a switch expression:

The code is arguably more readable, as it saves a significant number of characters—no need for case statements, variable declarations, and the bodies are now expressions. Additionally, unlike a regular expression switch, the compiler provides a warning (rather than an error) for any unhandled values. If a switch expression reaches the end without a match, an exception is thrown at runtime.

Switch expression fits particularly well with expression-bodied members:

C#8 also introduced extremely useful property patterns.

Notice that:

  • The var pattern can be used to introduce variables to handle property values.
  • A property pattern requires the object reference to be not null. Hence empty property pattern { } is a test for null check.

If the rectangle has a deconstructor, the expression Rectangle { Width: var x, Height: var y } can be simplified to  Rectangle(var x, var y). This is the new C# 8 positional pattern.

Finally C#8 introduced the tuple pattern to test multiple values at the same time:

C# 9: Combinator, Parenthesized and Relational Patterns

C#9 introduced combinator patterns: conjunctive and, disjunctive or and negated not patterns. This is especially useful to avoid repeating a variable in a complex boolean expression.

The code sample above also illustrates the parenthesized pattern. Here you can remove the parenthesis but they make the logic clearer.

Notice that the new negated not pattern constitutes a new syntax for null check: if(obj is not null) { ... }

C#9 also introduced relational patterns < > <= >=.

The relational pattern fits especially well when used with a property pattern as shown by the code sample below. Also, this example illustrates a nice C#9 addition: you can omit the underscore symbol in type pattern for a lighter syntax:

C# 10: Extended Property Pattern

C# 10 introduced an extended property pattern which is useful to nest property calls as illustrated by the code sample below:

C# 11: List and Slice Pattern

C#11 introduced list pattern matching. For example array is [1, 2, 3]  will match an integer array of the length three with 1, 2, 3 as its elements, respectively. Notice that:

  • The new slice pattern .. matches any sequence of zero or more elements.
  • The discard pattern _ matches any single element.

Consequently:

  • the pattern [_, >0, ..] means array.Length >= 2 && array[1] > 0.
  • the pattern [.., <=0, _] means array.Length >= 2 && array[^2] <= 0.

Notice that the slice pattern can only appear at most once in a list pattern.  Two slice patterns cannot check if an array contains a particular item:

slice-pattern-appear-only-once-in-list-pattern

List pattern matching can be useful to introduce variables like in this method:

The input of the list pattern matching can be any object that is both countable (with a property Length or count) and indexable (supporting the syntax [int]). For example, it can work on a string. When dealing with your custom types, you can enable them to function as inputs for list patterns by providing the required members.

List pattern matching is a recursive pattern. This means that elements of the list test can also be lists like for example:

Finally, notice that the var pattern can be used to check some patterns on the sub-array of a slice pattern like in the expression [.. var nMinusOneFirstElems, 37]. However, doing so is not recommended because this can lead the compiler to allocate a new array just for the check:

The same way a string can be allocated:

string-allocation-in-list-pattern

C# 12, 13, 14: No new Pattern Matching syntax

These versions didn’t introduce new Pattern Matching syntax.

See the list of C# 12 new features herethe list of C# 13 new features here and the list of C# 14 new features here.

Pattern Matching is no magic

Pattern matching is no magic. At first glance, one could think that a pattern expression is like a IQueryable LINQ expression: a language peculiarity that the compiler translates to a parametrized runtime object with special runtime processing. But it is not. Patterns are translated to traditional IL code. For example, let’s decompile these two methods:

Here is the IL code:

Pattern-Matching-Decompiled

Pattern Matching can Help Improve Performances

Let’s have a look at how the C# compiler compiles these three methods:

Here’s the resulting IL code as decompiled with ILSpy. Although all three implementations behave similarly, the C# compiler was able to apply an optimization in both pattern matching cases:

However, don’t just take our word for it—always verify the performance of critical code yourself using Benchmark.NET.

Don’t overuse Pattern Matching

One of the most prominent pattern matching usage (at least in documentation) is type pattern. Concretely the code should behave differently if a shape is a circle or a rectangle. But one must keep in mind that polymorphism is here for that. It would be terrible design to use pattern matching to compute a shape area instead of providing an abstract Area property within the base class Shape:

This would be a maintenance nightmare since a new kind of shape introduced would need its own area formula (a clear violation of the Open Close Principle). Also, we can expect poor performance since the virtual method table used at runtime to handle polymorphism is certainly faster than type compatibility checking.

Conclusion

Again C# Pattern Matching is to complex if / else statements what C# LINQ is to for / foreach loop: a nice and modern C# improvement to write more concise and readable code. The C# compiler can even rely on Pattern Matching to generate more performant code.

However, type pattern is not a replacement for polymorphism. You can keep it only for peculiar situations.

Finally – as underlined in the C# 7 section – C# pattern matching suffers from the limitation that only constant expressions can be used to match against. Maybe the C# team will relax this in the future but they need to find a good experience around exhaustiveness, particularly in switch expressions. With a non-compile-time-constant pattern, how the compiler could determine if the expression handles all cases? Another concern is when matching against values that trigger side effects – or even worse non-constant values – which would lead to undefined behavior.

 

 

 

Comments:

  1. Thanks for the clear summary! It’s nice to see how well C# is coming along, and I’m looking forward to playing around with the newer pattern types soon.

Comments are closed.