The first two lessons in this section introduced the is operator and switch expressions. Property patterns are the next building block: a way to match on an object's properties without writing chains of .Prop == checks or pulling values out into local variables first. They turn questions like "is this a shipped order to a US address with at least one item" into a single readable expression.
Without property patterns, asking "does this customer meet several conditions" looks like a chain of property access and comparisons. Each && is another place the reader has to follow the chain, and any one of them can fail with a null reference if you forget a guard.
The code works, but four things are going on in that if. The null guard, the active check, the points check, and the implicit reading of three different properties. A property pattern collapses the same logic into one shape that reads like a description of the data, not a sequence of checks.
The { IsActive: true, LoyaltyPoints: >= 1000 } part is a property pattern. The braces wrap a comma-separated list of PropertyName: pattern pairs, each of which has to match. The pattern also handles null automatically: if customer is null, the whole pattern fails without throwing. That's the second win after readability, the null guard is built in.
Property patterns showed up in C# 8 as part of the larger pattern matching expansion, and they grew through C# 10 with extended (dotted) property access. This lesson covers the syntax, the nesting rules, the capture form, and common mistakes.
The minimum form is expression is { Prop: pattern }. The pattern after the colon can be anything that's a valid pattern: a constant, a type pattern, a relational pattern, a logical combination of patterns, or another nested property pattern.
The first pattern uses a constant string. The second uses a relational pattern (> 50m), which the _Relational & Logical Patterns_ lesson covers in detail. Both forms are valid right-hand sides for a property in a property pattern.
You can list multiple properties separated by commas. All of them must match for the whole pattern to succeed. Think of the commas as an implicit and.
A property pattern with no properties at all ({ }) matches any non-null reference. That's occasionally useful as a null check that fits into a larger pattern, though is not null reads better when that's all you're doing.
The property pattern can sit right after a type name, which produces a combined "this object is of type T and has these property values" check in one expression. The type pattern runs first, so the property pattern only sees a value of the named type.
The variable after the pattern (shipped, review) is the captured value, already cast to the matched type. You don't need a separate cast. Only the OrderShipped branch fires because the incoming value isn't a ReviewPosted. The type test in the property pattern short-circuits before reading any properties.
This shape comes up constantly in code that processes a heterogeneous stream of objects: webhooks, message envelopes, command unions. The combined type-and-property check expresses both "what kind is this" and "what does it look like" in one place.
Ordering matters here. The more specific pattern (ItemAdded { Quantity: > 1 }) has to come before the looser one (ItemAdded single), or the looser one absorbs everything that would have matched the specific case. This is the same rule that applies to switch expression arms, and it's worth keeping in mind whenever you write overlapping patterns.
The right side of a property pattern can be another property pattern. That's what makes the feature recursive: you can keep drilling into properties without leaving the pattern.
The outer braces match the Order. Inside, Address: says "look at the Address property", and { Country: "US" } is itself a property pattern that has to match what's there. Each level handles its own null check: if order.Address is null, the inner pattern fails and the whole expression returns false, no exception.
You can nest as deeply as you want. The pattern reads top-down, mirroring the shape of the data.
The pattern reads almost like the path through the object graph, which is the whole point. The deeper the graph, the more value you get out of nesting compared to writing order.Address?.City?.Country == "US" && order.Address?.City?.Name == "San Francisco".
The diagram below shows how the matcher walks the nested pattern.
Every step has its own null guard, every step short-circuits on failure. The matcher never reads past a null reference, and the pattern's structure makes the order of checks obvious.
Nested patterns work, but they get verbose when you only care about one deep property. C# 10 added the extended property pattern, which lets you use dotted access directly in the property name slot.
Both forms produce the same match. The extended form is shorter and reads better when you only want one property from a deeper level. The nested form is better when you want multiple properties from the same nested object, because writing { Address.Country: "US", Address.City: "SF" } reads each Address. again, while { Address: { Country: "US", City: "SF" } } factors it out.
Both forms handle null the same way. order.Address.Country in the extended pattern doesn't throw if Address is null; the pattern fails instead.
All three cases work without exception. The matcher silently short-circuits when any link in the chain is null, which is what makes the feature safe to use on nullable object graphs.
The rule of thumb: use the extended (dotted) form for a single property from a deeper level. Switch to the nested form when you want two or more properties from the same nested object, because it groups them visually.
| Goal | Best form |
|---|---|
| One property from one level deep | Extended: { Address.Country: "US" } |
| Two properties from the same nested object | Nested: { Address: { Country: "US", City: "SF" } } |
| Properties from many different sub-objects | Mix both as needed |
| Pre-C# 10 codebase | Nested only |
var and DesignationPattern matching wouldn't be very useful if you couldn't get the matched value out. Property patterns support two ways to capture: a var pattern inside the property's value position, and a designation after a pattern that binds the whole matched value to a name.
Stock: var stock matches anything (because var always matches) and assigns the property's value to a new variable called stock. The second form, Stock: > 0 and var available, combines a relational pattern with a var pattern using and. The match only succeeds when Stock > 0, and available ends up holding the value when it does.
You can also capture the whole object after the pattern by writing a name at the end. This works exactly like the type-pattern designation shown earlier.
The premium variable is typed as Customer and bound to the matched value. You didn't need a cast, an as, or a separate is Customer c step. The pattern does the type test, the property checks, and the capture in one expression.
Property patterns compile to plain field/property reads and comparisons, the same code you'd write by hand. There's no reflection, no boxing of value-type property values, and no allocation for the pattern itself. The only thing to watch is calling the same expensive property twice in different parts of a pattern, which the compiler can't always collapse.
Property patterns shine inside switch expressions, where each arm is one shape you're testing for. The combination cuts down on boilerplate and makes branching logic read like a table of cases.
The arms are evaluated top to bottom, and the first matching arm wins. That's why the Total: >= 100m arm has to come before Total: >= 50m: the lower bound would otherwise swallow every order over 100. The discard _ at the end catches anything none of the property patterns matched (in this case, an order with a zero total or a null reference).
The same pattern works for state machines and lookup tables. Here's the order lifecycle expressed as property patterns over a single object.
The not null pattern in the value position is a common shape for "does this nullable property have a value?" Each arm asks one question, and the order encodes the priority. Cancellation wins over everything, delivered wins over in-transit, and so on.
Modern C# (8+) has nullable reference types enabled by default in most project templates. That changes how property patterns interact with nulls in two small ways worth being explicit about.
First, the pattern itself never throws on null. The outer object can be null, any nested property can be null, and the match just fails. You don't need ?. anywhere inside a pattern.
Second, when a pattern matches, the compiler narrows the matched value's nullability. After if (order is { Address: { } addr }), the variable addr is known to be non-null and the nullable-reference-type checker stops warning when you access its members.
The pattern { Country: { } country } reads as "Country is a non-null value, bind it to country." The empty { } in the value position matches any non-null reference. That's the idiomatic way to say "this property must exist" in pattern syntax.
If you want to match specifically on null, use the constant pattern null. To match anything except null, use not null.
Each arm asks "is this nullable property populated?" without ever risking a null reference. That's the kind of code where property patterns pay for themselves quickly.
CountProperty patterns work with any property, including Count on collections. That makes them a handy way to ask "is this list empty?" or "does this cart have more than one item?" without writing a separate length check.
The Items.Count pattern uses the extended property syntax to reach into the collection's Count property in one step. It works because List<T> exposes Count as a regular property. The same trick works with string.Length, Dictionary<,>.Count, and any custom type that exposes a count-like property.
The _List Patterns_ lesson introduces list patterns ([first, .., last]), which go further and let you match on individual elements. For "I just need to know the size," Count in a property pattern is the lightweight tool.
Reading Count on List<T> is O(1). On IEnumerable<T>, there's no Count property at all, so the pattern won't compile. Be careful with types that have a Count extension method but no instance property: property patterns can only reach instance members.
The relational and logical patterns (and, or, not, >, <, >=, <=) compose with property patterns in the property value position. The pattern after the colon can be any pattern, which includes any logical combination.
A few things worth pointing at. Stars: 1 or 2 uses an or pattern to match either of two constants without writing two arms. Body: { Length: > 50 } nests a property pattern inside the value position. And Body: null or "" chains a null match with an empty-string constant; both have the same "no useful body" meaning so the arm treats them together.
The _Relational & Logical Patterns_ lesson covers and, or, not, and the relational operators in depth. The point here is that they slot into property patterns wherever a value pattern is expected, with no extra syntax.
A few mistakes show up when first using property patterns. Most of them produce silent wrong results rather than compile errors, so they merit attention up front.
Misspelling a property name. If the property exists, the compiler accepts it. If it doesn't, you get a CS0117 error. The tricky case is when a property exists but is null-tolerant in your model: you can't accidentally use the wrong property without the compiler noticing, because property patterns bind by name to actual members.
The good news: property patterns don't tolerate typos. The compiler reports CS0117 immediately. This is one place where the pattern is stricter (and safer) than runtime reflection.
Forgetting the type when narrowing from `object`. When the variable's static type is object (or a base class), a property pattern alone won't compile because the property doesn't exist on the static type.
Add the type pattern (Customer { ... }) when the static type doesn't already declare the property. The compiler then sees the property and the pattern works.
Mixing nested and extended syntax in confusing ways. Both { Address: { Country: "US" } } and { Address.Country: "US" } are legal and equivalent. What's not legal is splicing them at one level:
The third form works on C# 10+ and chains dotted access all the way to Length. Pick one style per pattern and stick with it for readability.
Forgetting that an empty property pattern matches anything non-null. { } on its own is the "non-null" pattern. People sometimes write obj is { } thinking it's a tautology and wonder why the linter flags it. It isn't: it's a null check.
is { } is slightly older syntax for "is not null." On modern C# (9+), is not null reads better and the compiler treats them as equivalent.
Assuming property patterns always trigger property getters. They do call the getter. If a property has side effects (lazy initialization, logging, computation), the pattern will trigger them. The same warning applies to chained property access in nested or extended patterns.
The compiler doesn't deduplicate identical property reads inside a pattern. If you write { Value: > 5, Value: 7 }, the getter runs once per occurrence. For pure properties this doesn't matter, but for getters with cost or side effects, lift the value into a local first.
10 quizzes