C# Property Pattern with Examples
c# property pattern: Learn how to use the C# property pattern to match objects based on their properties, with practical examples and common pitfalls.
The C# property pattern lets you match an object against a set of property values within a pattern-matching expression. It was introduced in C# 8.0 as part of recursive pattern matching and is useful for writing concise, readable validation logic without manually chaining if-statements. This article covers the syntax, practical usage, and trade-offs of the property pattern for working developers.
Property Pattern Syntax
The property pattern extends the is expression and the switch statement or expression. Instead of matching a type only, you can match against one or more properties. The basic syntax looks like this:
public record Order(int Id, string Customer, decimal Total); bool IsLargeOrder(Order order) => order is { Total: > 1000 };
Here, order is { Total: > 1000 } checks that order is not null and that the Total property is greater than 1000. The property pattern uses a nested pattern for each property. You can combine multiple properties with commas:
bool IsPriorityOrder(Order order) => order is { Total: > 1000, Customer: "VIP" };
This checks both conditions. If order is null, the is pattern returns false. The property pattern is recursive, meaning the property value can itself be matched with another pattern, including another property pattern.
Using Property Patterns in Switch Expressions
Property patterns are particularly useful in switch expressions, where they replace multiple if-else conditions. Consider a shipping cost calculator:
public enum DeliveryZone { Local, Regional, International } record Package(double Weight, DeliveryZone Zone); decimal CalculateShipping(Package pkg) => pkg switch { { Zone: DeliveryZone.Local } => 5.0m, { Zone: DeliveryZone.Regional, Weight: <= 10 } => 10.0m, { Zone: DeliveryZone.Regional, Weight: > 10 } => 15.0m, { Zone: DeliveryZone.International, Weight: <= 5 } => 20.0m, { Zone: DeliveryZone.International, Weight: > 5 } => 35.0m, _ => throw new ArgumentException("Unknown shipping scenario") };
Each arm uses a property pattern. The _ discard acts as the default case. The switch expression evaluates arms in order, so the first matching arm wins. This ordering matters when patterns overlap; for instance, if you had a broad match before a specific one, the specific one would never run.
When writing property patterns in a switch expression, you do not need to include a type name explicitly if the input type is already known, but you can add it for clarity or to filter by type:
string Describe(object obj) => obj switch { Order { Total: > 1000 } => "Large order", Order => "Regular order", _ => "Not an order" };
Combining with Other Patterns
The property pattern can be nested with other patterns, such as type patterns, relational patterns, and logical patterns. For example, you can match a specific derived type and its properties:
abstract class Shape { } class Circle : Shape { public double Radius { get; init; } } class Rectangle : Shape { public double Width { get; init; } public double Height { get; init; } } string Classify(Shape shape) => shape switch { Circle { Radius: > 0 } => "Valid circle", Rectangle { Width: > 0, Height: > 0 } => "Valid rectangle", _ => "Unknown or invalid shape" };
You can also use logical operators and, or, and not within property values:
bool IsSpecialOrder(Order order) => order is { Total: > 100 and < 5000, Customer: not "Internal" };
This checks that Total is between 100 and 5000, and that the customer is not "Internal". The not pattern is useful for excluding a specific value.
Practical Example: Validation Logic
A common use for property patterns is to validate input objects at the boundary of an application. Instead of writing a long series of if-statements, you can express all rules in one switch expression. Suppose you have a request object:
record CreateUserRequest(string Email, string Name, int Age); string Validate(CreateUserRequest request) => request switch { { Email: null or "" } => "Email is required", { Name: null or "" } => "Name is required", { Age: < 18 } => "Must be at least 18", { Age: > 120 } => "Age is unrealistic", _ => "Valid" };
This approach keeps all validation in one place and makes it easy to see all rules at once. However, it returns only the first failing rule. If you need to collect all validation errors, a switch expression is not ideal; you would need a loop or a collection of predicates. For single-error validation, this pattern is clean and less error-prone than scattered if-statements.
Nested Property Patterns
Property patterns can be nested to access nested objects. For example, if an Order has a Customer object with an Address, you can match on the address's city directly:
record Customer(string Name, Address Address); record Address(string City, string Country); record Order(Customer Customer, decimal Total); bool ShipsToSeattle(Order order) => order is { Customer.Address.City: "Seattle" };
The extended property pattern uses dotted notation to reach nested members. This works because the property pattern is recursive: the inner property pattern { Address.City: "Seattle" } is applied to the Customer property. This avoids null checks on intermediate objects because the pattern automatically returns false if any intermediate property is null. In C# 10 and later, you can extend nested properties even further with the .. notation to access members of a nested object's property, but for most cases dotted access is sufficient.
Performance and Runtime Cost
The property pattern compiles to code that performs a null check and then accesses the relevant properties. Each property access is a simple getter call. There is no reflection involved, so the runtime cost is minimal, comparable to writing the equivalent manually. However, in hot paths, consider that accessing a property may trigger side effects if the getter is expensive. The pattern does not cache property values; it calls the getter each time the pattern is evaluated. For simple auto-properties backed by fields, this is negligible. For properties that compute values or perform I/O, the property pattern can be as expensive as the getter itself. In practice, you should use property patterns where you would otherwise write manual checks; the performance difference is negligible. Avoid using property patterns with properties that have side effects, because the pattern may evaluate the getter multiple times if used in a compound condition. The compiler may optimize some cases, but it is not guaranteed.
Field and Property Access
The property pattern works only with properties, not with fields. If you try to use it with a public field, you will get a compiler error. The same applies to indexed properties or methods. If you have a type with public fields, you must refactor to use properties or find an alternative approach. Also, the property pattern does not perform asynchronous property retrieval; it is a synchronous construct.
Common Pitfalls and Limitations
One common mistake is to forget the null check. The property pattern implicitly checks for null if the object reference is not known to be non-null. However, if you have a nullable reference type, the compiler may warn but still allow. Also, the property pattern does not handle null property values gracefully in some cases: if you match a property against a pattern that expects a non-null value, the pattern fails only if the property is null. For example, { Name: "John" } fails if Name is null, but { Name: not null } succeeds. The is pattern with a property pattern does not throw if the object is null; it simply returns false. But in a switch expression, if all arms fail and there is no discard arm, an exception is thrown. This is a runtime SwitchExpressionException. Always include a default _ arm unless you can prove one of the patterns always matches.
Another limitation is that property patterns do not capture the matched value directly. To get the object itself, you can use the var pattern combined with the property pattern:
if (order is { Total: > 1000 } largeOrder) { // largeOrder is the same object as order, but only if the pattern matched }
But this is a bit redundant; you can simply use the original variable. Property patterns are not a substitute for deconstruction; they are for matching, not extraction. If you want to extract properties into local variables, consider using the positional pattern with deconstructors.
Maintainability and When to Use Property Patterns
Using property patterns reduces the number of lines and makes the logic more declarative. However, for complex validation that requires returning multiple errors, a more traditional approach may be more readable. As a rule, use property patterns when:
- The logic is a simple equality or range check on one or two properties.
- The logic is a decision table that can be expressed as a switch expression.
- The input type is known and you want to avoid nested ifs.
Avoid property patterns when:
- You need to perform complex side-effectful validation.
- You need to collect all validation errors.
- The property getters are expensive or have side effects.
Property patterns integrate well with records, which provide value-based equality and concise syntax. However, pattern matching is not dependent on records; it works with any class or struct. For .NET developers working with C# 8 or later, property patterns are a valuable tool for writing intent-revealing code.
Advanced Usage: Combining with var and Lists
While the property pattern is designed for object properties, you can combine it with list patterns in C# 11 to match collections of objects. For example, you can check if the first element of a list is a certain type and has a property:
string DescribeList(IEnumerable<object> items) => items switch { [{ } first, ..] when first is Order { Total: > 100 } => "Starts with large order", _ => "Other" };
This uses a list pattern and a property pattern. The when clause adds an additional guard. This shows the power of recursive patterns, but it also increases complexity. Use such combinations sparingly, as they can hurt readability if overused.
Compatibility and Version Requirements
Property patterns require C# 8.0 or later. If your project targets an older language version, you cannot use them. Most modern .NET projects use C# 9 or later, so this is rarely a constraint. Keep in mind that property patterns are a compile-time feature; the generated code runs on any .NET runtime that supports the language version, but the runtime must support the necessary pattern-matching infrastructure. In practice, running on .NET Core 3.0 or later is fine. If you use extended property patterns (dotted access) or .. notation, you may need C# 10 or later depending on the feature. The examples in this article work with C# 10 and .NET 6 or later.
The property pattern is a compact way to write branch logic that would otherwise require multiple conditionals. Understanding how it compiles and when to apply it will help you write code that is easier to maintain and less prone to null-reference mistakes. For code that requires performance-critical checks, remember that the property pattern is not a free lunch; it calls getters. But for most business logic, the clarity gain outweighs the minor cost.