NDepend Blog

Improve your .NET code quality with NDepend

C# Pattern Matching Explained (2026)

June 6, 2026 9 minutes read

C# Pattern Matching Explained

Last updated: May 2026. Covers C# 7 through C# 14.

C# Pattern Matching lets you test whether an expression has a specific shape (a type, a value, a structure) and pull data out of it, all in one syntax. After eight years of pattern matching landing in C# 7, the syntax has grown into something genuinely useful. It has also grown into something easy to misuse, which is why this guide is opinionated about when to reach for it and when to leave it alone.

This article walks through every C# pattern matching syntax with code you can actually copy: the is operator, switch expressions, type patterns, property patterns, list patterns, and much more. Each section calls out the C# version that introduced the syntax so you know what is available in your target framework.

C# Pattern Matching Cheat Sheet

If you only want the summary, here it is:

Pattern Since Example Use when
Null C# 7 x is null You need a real reference null check, bypassing operator overloads.
Constant C# 7 code is 404 The value to compare is a compile-time constant.
Type / Declaration C# 7 shape is Circle c Types are unrelated by inheritance. Otherwise, use polymorphism.
Discard / var C# 7 _ => ... Default arm of a switch, or as a placeholder inside other patterns.
Switch expression C# 8 x switch { ... } You want a value out of the switch, with exhaustiveness checking.
Property C# 8 { Age: >= 18 } Decision depends on object properties.
Positional C# 8 (var x, var y) The target type has a Deconstruct method or is a positional record.
Tuple C# 8 (a, b) switch { ... } Decision depends on a combination of inputs.
Logical (and, or, not) C# 9 is not null Composing patterns without repeating the variable.
Relational C# 9 is >= 100 Numeric range checks.
Extended property C# 10 { Address.City: "Paris" } Matching on a nested property without nested braces.
List / slice C# 11 arr is [1, .., 9] Small-arity sequence inspection. Beware allocations.

The rest of this article expands every row.

What is C# Pattern Matching?

C# pattern matching is a language feature that tests whether an expression has a specific shape — a type, a constant, or a structure — and, on a match, extracts its data into typed variables. It works with the is operator, switch statements, and switch expressions, and was introduced in C# 7.

A pattern is a description of what an expression should look like. Pattern matching is the act of testing an expression against that description. The result is a boolean, and on a successful match the language can also extract data into typed variables.

Compared to long chains of if clauses, pattern matching gives you three things that matter:

  • The expression under test is named once, not five times.
  • Type checks expose the matched value as a typed variable. No casts.
  • Switch expressions catch missing cases at compile time.

Here is the canonical before/after. Both snippets do the same job. The second one combines a type pattern with a property pattern, and the difference in readability is hard to argue with:

Patterns live in three places: the is operator, the classic switch statement, and the switch expression introduced in C# 8. Learn a pattern once and it works in all three.

The is Operator: Where Pattern Matching Starts

The is operator is the doorway. Any pattern can be tested with expression is pattern, and the result is a bool. The same patterns also work in switch statements and switch expressions.

Pattern matching is not magic, it is only syntactic sugar. At first glance you might assume it works like an IQueryable LINQ expression, where the compiler builds some runtime object the framework then interprets. It does not. The C# compiler lowers patterns to plain IL, the same IL you would write yourself by hand. Compare these two methods:

Decompiled, they look essentially identical:

C# Pattern Matching Decompiled to IL

That property has practical consequences. Pattern matching is not slower than equivalent boolean expressions, and as we will see in the performance section, the compiler will occasionally rewrite a pattern more aggressively than it ever rewrites a plain || chain.

Checking for Null: is null and is not null

The simplest pattern matches against null:

Since C# 9 the negated form is not null is available, and the C# style guide now recommends it over != null:

Why prefer the pattern form over == null? Because == can be overloaded, and there are classes in the wild whose operator == returns true for an instance compared against null (think Unity’s UnityEngine.Object). The pattern form is always a true reference comparison. I have been bitten by this exactly once, and once was enough.

Matching Literal Values: the Constant Pattern

The constant pattern (C# 7) compares against a compile-time constant:

What counts as a constant:

  • integer or floating-point numerical literals
  • char and string literals
  • the boolean values true and false
  • enumeration values
  • the name of a declared const field, or null

And here is the catch that has hurt this feature’s adoption more than anything else: the value baked into a pattern must be a compile-time constant. If the value lives in an appsettings.json, in a database, or in a static readonly field initialised at startup, you cannot use it in a pattern. The compiler will reject the code:

C# Pattern Matching Requires a Compile-Time Constant

This restriction shows up in real codebases often enough that I keep mentioning it. The C# team has not relaxed it yet, and the reasons (exhaustiveness, side effects on the matched-against expression) are not trivial.

The Type Pattern: Test and Capture in One Step

If you only learn one pattern, learn this one. The type pattern (C# 7) checks whether an expression has a given type and, on success, binds the value to a new variable of that type. It replaces the old is-plus-cast dance:

The type pattern earns its keep in switch statements over types that have no common base class. The BCL is full of these situations. Here is a small one: counting the elements of an arbitrary sequence in O(1) when possible:

Order matters. The compiler will reject case clauses it can prove unreachable. Put ICollection<T> before List<T> and the compiler tells you why:

C# Pattern Matching Clause Order Matters

Declaration Pattern vs Type Pattern: a Distinction Worth Knowing

The C# language specification actually splits this into two named patterns, and those names turn up in compiler messages and on Stack Overflow, so it pays to recognise them:

  • The declaration pattern shape is Circle c checks the run-time type and binds a new variable c of that type.
  • The type pattern shape is Circle only checks the type and binds nothing. Before C# 9 you had to write Circle _ for this case; since C# 9 the bare type name is enough.

Both reject null: neither form matches a null value, regardless of the static type of the operand. That property makes the declaration pattern the cleanest way to test a nullable value type and unwrap it to its underlying type in a single step:

The when Clause

Inside a switch, the when keyword adds an arbitrary boolean condition on top of a pattern. This is where you escape the compile-time-constant restriction:

The when keyword only works in a switch. It is not allowed in an if statement, even in the latest C# version:

C# when Keyword Cannot Be Used in if Expression

Discard and Var Patterns

The discard pattern _ matches anything and binds nothing. Use it for the default arm of a switch, or as a placeholder inside other patterns where you do not care about a value. One quirk to file away: a bare _ is allowed in a switch expression, but in an is expression or a switch statement you have to spell it var _ instead.

The var pattern (C# 7) is a different animal. It matches anything, including null, and binds the matched value to a new variable of inferred type:

Here is my opinion on this one. The var pattern is almost never what you want in production code. The fact that it matches null is a footgun in disguise, and the resulting variable is in an undefined null state. The legitimate use case is roughly one: capturing values inside other patterns when anonymous types make naming impossible. Outside that, prefer a plain assignment. Reviewers should not have to remember which is the var pattern and which is a type-inferred declaration.

Switch Expressions: the Best Thing C# 8 Shipped

The switch expression (C# 8) is the feature that made me change how I write decision logic. It is an expression, not a statement, so it returns a value:

What you gain over a classical switch statement: no case keyword, no break, no temporary variable to hold the result, and (the big one) compile-time exhaustiveness checking. When the compiler can prove some inputs would fall through the end of the switch, it emits a warning:

C# Switch Expression Compiler Warning for Unhandled Value

If you do reach the end at runtime, a SwitchExpressionException is thrown.

Switch expressions pair beautifully with expression-bodied members:

One word of caution. Switch expressions look great with three arms and unreadable with fifteen. When the arm count climbs, extract the logic into named helper methods, or fall back to a switch statement with explicit blocks. I have reviewed PRs with thirty-arm switch expressions across multiple files, and nobody could tell me what they did without running them.

Property Pattern: Matching on Object Members

The property pattern (C# 8) tests the values of one or several properties. Combined with the type pattern it shines:

A couple of points that trip people up:

  • The var pattern inside a property pattern is the legitimate use case for var. Here it captures a property value into a local.
  • A property pattern requires the receiver to be non-null. The empty braces form { } is therefore a fancy not-null check. You can even bind it, as in obj is { } notNull, to get a non-null variable without naming a type.

Extended Property Pattern (C# 10): Dot Access

Before C# 10, matching on a nested property meant nesting braces. C# 10 added dot notation, which is a small change with an outsized impact on readability:

Resist the temptation to chain four or five dots. By the time you write { Order.Customer.Address.City.Length: > 0 }, you are violating the Law of Demeter and writing code nobody can refactor.

Positional Pattern: Matching via Deconstruction

The positional pattern (C# 8) uses an object’s Deconstruct method (see Deconstruction in C#) or its positional record syntax to match by position rather than by property name:

Positional patterns work best with C# records, where the deconstructor is generated for you and parameter order is part of the public API. Hand-written Deconstruct methods on mutable classes are a source of bugs (was width first, or height?), so I rarely write them for that purpose. One rule the compiler enforces without mercy: the order of the elements in the pattern must match the order of the out parameters in Deconstruct, because the generated code calls that method directly.

Tuple Pattern: Multiple Values, One Decision

The tuple pattern (C# 8) tests several values at once by wrapping them in a tuple. It shines when the decision depends on a combination of inputs:

This is also the cleanest way to model a state-machine transition table in C#, with the current state and the incoming event as the tuple. The values are wrapped in a C# ValueTuple.

Logical Patterns: and, or, not

The logical patterns (C# 9) compose simpler patterns without forcing you to repeat the variable name:

The example also illustrates the parenthesized pattern (C# 9). The parentheses are optional, but they pin down precedence and make the code readable. They matter more than they look: the combinators bind in the order not, then and, then or, so c is not >= 'a' and <= 'z' does not mean what most people expect. Use the parentheses.

The not pattern is the basis of the is not null idiom we already saw. For more on why the C# team added these keywords, see the design motivation behind the and, or, not keywords.

Relational Patterns: <, >, <=, >=

Relational patterns (C# 9) compare against a constant. They turn ladder-shaped else if chains into something compact:

Combined with property patterns they get even better:

Notice the small C# 9 quality-of-life change: when you only care about a type, the discard after the type name is no longer required. One gotcha worth remembering: a relational pattern never matches null or a value that fails to convert to the constant’s type, so you still need an explicit arm for those.

List Pattern and Slice Pattern

The list pattern (C# 11) is the most expressive pattern matching addition since C# 8. It tests a sequence element by element. array is [1, 2, 3] matches an array of length 3 with those exact values.

Two helpers do the heavy lifting:

  • The slice pattern .. matches zero or more elements.
  • The discard pattern _ matches a single element.

So:

  • [_, >0, ..] is shorthand for array.Length >= 2 && array[1] > 0.
  • [.., <=0, _] is shorthand for array.Length >= 2 && array[^2] <= 0.

A list pattern can contain at most one slice. Two slices to search for an element anywhere in an array is not allowed, and the compiler is clear about it:

C# Slice Pattern Can Appear Only Once in a List Pattern

A typical use is small-arity algorithms where you want named locals for the first few elements:

The input to a list pattern can be anything that is both countable (has a Length or Count property) and indexable (supports [int], and the ^ end-relative form from the index and range operators). That covers arrays, List<T>, Span<T>, ReadOnlySpan<T>, string, and any custom type that exposes the right members:

List patterns are recursive. The elements of a list pattern can themselves be patterns, including nested list patterns:

This recursive nature is what makes list patterns so good at parsing semi-structured data, like CSV rows where some lines carry more columns than others. You match on the shape of the line instead of writing index arithmetic by hand.

The Allocation Trap You Need to Know About

Here is a subtlety that catches people. The var pattern can capture the result of a slice, as in [.. var nMinusOneFirstElems, 37]. Do not do this in hot paths. The compiler will emit a hidden array allocation to materialize the slice:

C# Array Allocation in List Pattern

The same hidden allocation hits strings:

C# String Allocation in List Pattern

If you are processing millions of items, that allocation per match is a hot-path killer. Either drop the slice capture, or switch to Span<T> so the compiler can elide the allocation.

Pattern Matching Performance: When the Compiler Actually Wins

Pattern matching is not just syntactic sugar. There are cases where the compiler rewrites a pattern into IL that a hand-written boolean expression does not get. Compare:

Decompiled with ILSpy:

The two pattern matching variants get a range-based comparison the plain || chain does not. That said, the JIT is good at this kind of thing too, so the runtime delta is usually negligible. Never trust a microbenchmark argument made by a blog post. Run your own measurements with BenchmarkDotNet on the hot path that actually matters.

Known Limitations of C# Pattern Matching

Eight years in, the syntax still has rough edges:

  • Only compile-time constants can appear inside a pattern. Configuration values, database values, and runtime computations cannot. This is the single biggest reason pattern matching is not used more in business code.
  • The when clause is restricted to switch. You cannot attach a when to an if.
  • The slice pattern can appear at most once in a list pattern.
  • List patterns with captured slices can trigger hidden allocations.
  • Exhaustiveness checking is best-effort. A switch expression over a closed hierarchy of classes is not provably exhaustive to the compiler the way an F# discriminated union would be.

When Not to Use C# Pattern Matching

Most tutorials introduce pattern matching with a type pattern over a shape hierarchy. That is, with respect, the worst possible first example. It sells you on the syntax while quietly demonstrating an anti-pattern.

Consider:

Every new shape you ever add forces a hunt for every such switch in the codebase. That is the textbook violation of the Open/Closed Principle. Worse, the virtual method table is faster than repeated type-compatibility checks at runtime. An abstract Area property on the Shape base class wins on both maintainability and performance.

Pattern matching earns its place when:

  • The types you switch on are unrelated by inheritance. The Count example earlier is a good one.
  • You are decoding structured data, like a parser or a state machine.
  • You are writing a leaf piece of conditional logic over primitives, where polymorphism would be overkill.

If you find yourself adding a third type pattern arm to a switch on a domain object, stop and ask whether the right answer is a virtual method.

C# Pattern Matching by Version

For readers checking what is available in their target framework:

  • C# 7: is operator extended, null pattern, constant pattern, type pattern, declaration pattern, discard pattern, var pattern, when clause.
  • C# 8: switch expression, property pattern, positional pattern, tuple pattern.
  • C# 9: logical patterns (and, or, not), parenthesized pattern, relational patterns.
  • C# 10: extended property pattern (dot notation for nested members).
  • C# 11: list pattern and slice pattern.
  • C# 12, 13, 14: no new pattern matching syntax. See C# 12 new features, C# 13 new features, and C# 14 new features.
  • C# 15 (preview): union types are on the way, and when the value under test is a union the pattern automatically unwraps it to the underlying value. It is documented but not yet a shipped, supported feature, so treat it as something to watch rather than to lean on in production.

The C# language design discussions for these features are public on the dotnet/csharplang repository if you want the design history.

C# Pattern Matching FAQ

What is pattern matching in C#?

Pattern matching in C# is a feature that tests whether an expression has a specific shape (a type, a value, a property, a structure) and optionally extracts data from it. Patterns work with the is operator, switch statements, and switch expressions.

When was pattern matching introduced in C#?

Pattern matching arrived in C# 7.0 (2017) with the null, constant, type, discard, and var patterns. Every C# version up to C# 11 added new pattern syntax. C# 12, 13, and 14 added nothing new in this area.

What is the difference between a switch statement and a switch expression?

A switch statement is a control-flow construct that executes one of several blocks of code. A switch expression, added in C# 8, is an expression that produces a value. Switch expressions are more compact, support compile-time exhaustiveness checking, and integrate cleanly with expression-bodied members.

What is the difference between the declaration pattern and the type pattern?

The declaration pattern x is Circle c checks the run-time type and binds a typed variable. The type pattern x is Circle only checks the type and binds nothing; before C# 9 you had to write Circle _ for that. Both reject null.

How do I check for null using pattern matching in C#?

Use x is null for the null check and x is not null (C# 9 and later) for the negated form. These always perform a real reference comparison and ignore any user-defined operator ==. Prefer them over == null and != null.

Is C# pattern matching faster than if-else?

Usually the same, sometimes faster. The compiler applies optimisations to pattern matching that it does not apply to equivalent || chains, such as range-based comparisons for constant sets. Always measure with BenchmarkDotNet before treating any specific case as a perf win.

Can I match against a non-constant value in C#?

No. Pattern values must be compile-time constants. Workarounds: use a when clause inside a switch statement, or fall back to a regular if. This restriction is the main reason pattern matching is not used more in business logic.

Which C# version introduced list patterns?

List patterns and the slice pattern .. arrived in C# 11. They match a sequence element by element and work on any type that is both countable (Length or Count) and indexable ([int]), including arrays, List<T>, Span<T>, and string.

Does pattern matching work with records in C#?

Yes, and records are the ideal target for positional patterns. A positional record generates a Deconstruct method for you, so you can match by position with syntax like point is (0, 0) and keep parameter order as part of the public API.

Should I use the type pattern instead of polymorphism?

No. If your types share a base class or interface, polymorphism is the right tool. Use the type pattern for types unrelated by inheritance (like the IEnumerable<T> / ICollection<T> / Array example in this article), for parsing, and for state machines.

Conclusion

Pattern matching is to if/else what LINQ is to for/foreach: a more concise way to write the same logic, sometimes with better generated code. Almost a decade later with a new C# version released per year, it has matured into a feature you can trust in production.

The one habit to break is treating the type pattern as a replacement for polymorphism. It is not. Keep it for the cases where polymorphism does not apply, and your codebase will thank you.

The one wish I still have is non-constant matching. The C# team has not shipped it, and I understand why (exhaustiveness, side effects), but a story for that would unlock a lot of business-logic use cases that currently live in plain if chains.

This article is brought to you by the team behind NDepend — a proven .NET static analysis tool for improving code maintainability, security, and overall quality. Whether you’re modernizing a legacy .NET application or starting fresh in C#, get started with your free full-featured trial today!

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.

Leave a Reply

Your email address will not be published. Required fields are marked *