How to Use Property Patterns in Modern C

Property patterns in modern C# provide a concise, declarative syntax to test the properties and fields of an object against specific values, conditions, or nested structures. First introduced in C# 8 and significantly expanded in subsequent versions, property patterns eliminate repetitive null checks and boilerplate comparison code. This article explains the core mechanics of property patterns, demonstrates their integration with modern switch expressions and logical combinators, and covers extended nested patterns introduced in recent C# updates.

Basic Syntax and Matching

A basic property pattern matches an expression when an object is non-null and its specified properties match the given constants or conditions. The syntax uses curly braces { } containing one or more property-value pairs.

public record Order(int Id, decimal Total, string Status);

public bool IsEligibleForFreeShipping(Order order)
{
    // Matches if order is non-null and Status is "Completed" with Total >= 50
    return order is { Status: "Completed", Total: >= 50m };
}

In this example, the runtime ensures order is not null before evaluating Status and Total. If order is null, the pattern safely evaluates to false without throwing a NullReferenceException.

Using Property Patterns with Switch Expressions

Property patterns are commonly paired with switch expressions to handle multi-branch logic cleanly:

public decimal CalculateDiscount(Order order) => order switch
{
    { Status: "Cancelled" } => 0m,
    { Total: >= 100m, Status: "Active" } => 0.15m,
    { Total: >= 50m, Status: "Active" } => 0.05m,
    { } => 0m,       // Matches any non-null Order not caught above
    null => throw new ArgumentNullException(nameof(order))
};

The empty pattern { } serves as a match-all for any non-null instance of the type, providing a clear distinction between unhandled values and actual null references.

Relational and Logical Combinators

Starting in C# 9, property patterns can incorporate relational operators (<, <=, >, >=) and logical combinators (and, or, not):

public record Customer(int Age, int LoyaltyPoints, bool IsSubscribed);

public bool QualifiesForVip(Customer customer) => customer is
{
    Age: >= 18,
    LoyaltyPoints: > 500 or (>= 250 and not 300),
    IsSubscribed: true
};

This syntax allows multiple constraints on individual properties without separating them into distinct if statements.

Extended Property Patterns (C# 10+)

Before C# 10, inspecting nested properties required deeply nested braces, which could become difficult to read:

// C# 8 and C# 9 nested property pattern
if (employee is { Department: { Location: { City: "Seattle" } } })
{
    // ...
}

C# 10 introduced extended property patterns, enabling dot notation within the pattern:

// C# 10+ extended property pattern
if (employee is { Department.Location.City: "Seattle" })
{
    // ...
}

Each referenced property in the chain is automatically verified for null. If Department or Location is null, the pattern resolves to false without raising a runtime error.

Pattern Variable Assignment

You can capture either the root object or an individual property value while evaluating the pattern using the var keyword or type patterns:

if (order is { Total: var total and > 100m } highValueOrder)
{
    Console.WriteLine($"Order {highValueOrder.Id} qualified with a total of {total}.");
}

Here, total receives the evaluated value of order.Total, and highValueOrder receives the reference to order, scoped to the conditional block.

Summary of Key Behaviors

  • Implicit Null Safety: Property patterns evaluate to false if the target object—or any property accessed via extended dot notation—is null.
  • Type Independence: Property patterns do not require classes to implement a Deconstruct method, unlike positional patterns.
  • Composability: They integrate seamlessly with relational operators, logical operators, variable declarations, and switch expressions.