Back to Blog
C#

C# Logical Pattern

c# logical pattern: Understand C# logical patterns and/or/not, how they combine with property and type patterns, and when they simplify data-validation code.

pattern matchingC#logical operatorsswitch expressions
A visual of logical gates representing C# logical pattern combinators and, or, and not.

Modern C# pattern matching includes logical patterns built from the keywords and, or, and not. They apply boolean logic to pattern evaluation, letting you express complex conditionals in a single concise expression. For example, if (temperature is > 0 and < 100) checks a range without compound && conditions. C# logical patterns appear in if statements, switch expressions, and switch statements, giving a readable way to combine multiple pattern clauses.

Logical Pattern Operators and Their Semantics

The three logical pattern combinators are and, or, and not. Their behavior follows standard boolean algebra, but they apply to pattern outcomes rather than boolean values.

  • and requires both subpatterns to match.
  • or matches when at least one subpattern matches.
  • not matches when the following subpattern does not match.

These combinators can be nested and combined with other patterns, such as relational, property, type, and var patterns. Precedence from highest to lowest is not, then and, then or. Use parentheses when the grouping is not obvious.

Consider a temperature classification example:

static string DescribeTemperature(int celsius) { return celsius switch { < 0 => "Freezing", >= 0 and < 20 => "Cool", >= 20 and < 30 => "Warm", >= 30 and < 40 => "Hot", >= 40 => "Extremely hot" }; }

Here each arm uses and to bind two relational patterns into a range check. This avoids chained && expressions and makes the range boundaries obvious at a glance.

Using and to Combine Relational or Type Patterns

and is the most common logical pattern. It is useful when a value must satisfy two independent conditions. Because relational patterns do not accept arbitrary arithmetic expressions, keep calculations in ordinary code and use and for pattern-based range checks.

For example, the following property pattern applies and to both dimensions of a rectangle:

if (shape is Rectangle { Width: > 0 and <= 100, Height: > 0 and <= 100 } r) { // r is a Rectangle with dimensions in the range 0..100 }

You can also apply and inside each component of a positional pattern when working with tuples or types that support Deconstruct, as shown in the final section.

Using or to Match Multiple Alternatives

The or pattern lets a single condition match multiple alternative shapes. For example, to accept either a Uri or a string:

if (input is Uri or string) { // input is a non-null Uri or a non-null string }

Variable binding inside or patterns is restrictive: all alternatives must declare the same set of variable names with the same types. In practice, or works best when you only need to test alternatives, not extract a value common to both branches.

A common practical example is checking whether a value is null or empty:

if (value is null or "") { Console.WriteLine("Value is null or empty string"); }

Using not for Negation

not matches when the following pattern does not match. This is useful for guarding against a specific case before processing. For instance:

static bool HasContent(string? input) => input is not null and not "";

Here not null and not "" are combined with and. You can also use not in a switch expression to define a fallback arm:

string Size(double length) => length switch { > 100 => "Large", > 50 => "Medium", not > 0 => "Non-positive", _ => "Small" };

In this snippet, not > 0 catches zero or negative values. It is equivalent to <= 0, but the not form may be more readable when you want to negate a positive condition.

Combining Logical Patterns with Property and List Patterns

The power of logical patterns increases when you combine them with property patterns, list patterns, and positional patterns. For example, validate an object with several required properties:

record Product(string Name, decimal Price, bool InStock); bool IsDeal(Product p) => p is { Price: <= 20, InStock: true };

You can also use logical patterns inside property patterns to express thresholds:

bool IsUrgent(Order o) => o is { Total: > 1000 or < 0 };

This matches orders with a total over 1000 or a negative total. The or pattern is nested inside the property pattern, making the condition precise.

List patterns integrate with logical patterns and guards. For example, a switch on the first element of an array:

string FirstOrNone(int[] values) => values switch { [] => "Empty", [var first, ..] when first is > 0 => "Positive first", [var first, ..] when first is < 0 => "Negative first", [var first, ..] => "Zero first" };

Here the when clause uses relational patterns. You could also write when first is > 0 and < 100 to express a range inside the guard.

When Logical Patterns Reduce Readability

Logical patterns are concise but can become dense. Overusing not with complex subpatterns can reduce clarity. For instance, the double negation in x is not (not (> 0 and < 100)) says the same thing as x is > 0 and < 100, but is much harder to read. Prefer the straightforward form. The goal is to make conditions clearer, not to pack as many operators as possible into one line.

Also, not patterns cannot declare variables. This means you cannot write x is not int i to capture a non-int value. Use a separate if or switch arm instead.

Performance and Maintainability Considerations

Logical patterns are evaluated at runtime like any other pattern. Simple cases, such as and for numeric ranges, can compile to efficient comparisons. In most code, readability is the main reason to choose logical patterns, not a measurable performance difference.

More important is the maintainability benefit. Logical patterns keep decision logic in one place, especially when combined with switch expressions. For example, instead of a long chain of if-else if blocks, you can use a switch expression with clear arms:

static decimal ApplyDiscount(Order order) => order switch { { Total: > 1000 and < 5000 } => 0.1m, { Total: >= 5000 } => 0.15m, _ => 0m };

This structure is easier to review and modify because each arm is a self-contained condition that maps to a result.

Logical patterns are language syntax. You need a C# compiler that supports them—essentially C# 9 or later. If you are working in a mixed-version codebase, verify the target framework compiles with the correct language version.

Compatibility and Language Version Support

Logical patterns were introduced in C# 9. If you are using an older language version, you must use traditional && and ||. Most modern .NET projects target C# 9 or later, so this is rarely a constraint, but when editing legacy code, be aware that the compiler may reject logical patterns without the appropriate <LangVersion> setting.

Here is a quick mapping of logical pattern usage to C# versions:

PatternIntroduced inExample
Logical andC# 9a is > 0 and < 10
Logical orC# 9a is null or ""
Logical notC# 9a is not null
Extended property patternsC# 10obj is { A.B: > 0 }

Keep this in mind when explaining code to teammates or adopting patterns from more recent C# versions.

Using Logical Patterns to Replace Complex Guard Clauses

A common refactoring is to move null checks and shape tests into a single pattern. For example, a guard that validates a request object:

if (request is { User: not null, Permissions: { } perms } && perms.Contains("admin")) { // allowed }

Here the property pattern and not null guard make the null checks visible before perms is used. This still uses && for the permission check, but the pattern removes several nested null checks.

A more direct replacement is when you have a series of && checks that all apply to the same variable. For example:

if (score >= 0 && score <= 10 && !isOverridden)

Could be rewritten as:

if (score is >= 0 and <= 10 && !isOverridden)

This groups the range constraints on score into a single pattern, making the condition's intent clearer. It is a small change, but over a large codebase, such simplification improves readability.

Common Mistakes with Logical Patterns

One mistake is misplacing not. For example, x is not > 0 and < 10 parses as x is ((not > 0) and < 10), not as x is not (> 0 and < 10). Because not binds tighter than and, parenthesize when combining not with and/or.

Another mistake is trying to bind a variable inside a not pattern. The following is invalid:

if (x is not int i) // compiler error

You cannot declare i in a not pattern because the negation means there is no matched value to assign. Use a separate check instead.

Finally, or patterns make variable extraction awkward. If one alternative declares a variable, every alternative must declare the same variable with the same type; after the whole or pattern matches, the variable is assigned because the matching branch assigns it. For example, x is int i or string s is invalid because the alternatives declare different types. In most code, use an or pattern to test alternatives and use a separate switch arm when you need the matched value.

Writing a Small Validation Utility with Logical Patterns

The following utility rejects null, empty, and over-long strings, and then checks that every remaining character is alphanumeric:

public static bool ValidateInput(string input) { return input switch { null or "" => false, { Length: > 100 } => false, _ => input.All(char.IsLetterOrDigit) }; }

The first arm combines null and empty tests with or. The property pattern in the second arm is reached only when input is not null because the first arm handled that case.

Final Technical Note: Using Logical Patterns in Recursive Patterns

Logical patterns can be nested inside recursive patterns, such as positional or list patterns. For instance, matching a point inside a 0..100 square:

if (point is (>= 0 and <= 100, >= 0 and <= 100)) { // point is within the 0..100 square }

This positional pattern applies and within each component. It reads as "x is between 0 and 100 AND y is between 0 and 100." That is a natural fit for coordinate or grid-based logic. If a recursive pattern combined with logical patterns becomes too complex, extract the condition into a method with a descriptive name.

c# logical pattern: Practical Usage and Code Examples | RYUSLOG DEV