C# not pattern pitfall

Posted by on Thu, Apr 25, 2024

Using the pattern capabilities in C# can make code more readable and easier to understand but can come with some pitfalls that could catch you off guard if you are not careful.

Pattern power

Patterns can be especially useful when used with Enumerations. For example, given the following enum:

1enum OrderStatus
2{
3    Placed,
4    Processing,
5    Shipped,
6    Delivered
7}
8
9OrderStatus orderStatus = OrderStatus.Placed;

if you wanted to check if the order status was one of two potential values without using patterns you would typically do this:

1if (orderStatus == OrderStatus.Placed || orderStatus == OrderStatus.Processing)
2{
3    // do something
4}        

but with a pattern, you don’t need to repeat the variable that you are checking:

1if (orderStatus is OrderStatus.Placed or OrderStatus.Processing)
2{
3    // do something
4}

This expression is shorter in length and follows a more natural way of reading. But what if you wanted to check if the order status was not one of two different values?

With great power…

On the surface, you would right it like this, right?:

1if (orderStatus is not OrderStatus.Shipped or OrderStatus.Delivered)
2{
3    // do something
4}

This compiles and appears to be correct. Wouldn’t you naturally assume that you would enter the body of the if block when the order status is not Shipped or Delivered? If you run this code with orderStatus = OrderStatus.Shipped you will skip the body of the if statement… as expected. However, if you set orderStatus = OrderStatus.Shipped you will enter the body of the if block!! But why?

… comes great responsibility

The reason is that the C# compiler is actually interpreting the if expression as if it had been written like this (which can be confirmed with unit tests):

1if (orderStatus is not OrderStatus.Shipped || orderStatus is OrderStatus.Delivered)
2{
3    // do something
4}    

now that you understand that, your first few thoughts might be to try and mess with the combinations of the is not and and/or keywords but the only syntax that is going to be valid is to group the conditions being checked inside of parentheses as follows:

1if (orderStatus is not (OrderStatus.Shipped or OrderStatus.Delivered))
2{
3    // do something
4}

Once it is written this way, you will only enter the body of the if block when the order status is not equal to OrderStatus.Shipped or OrderStatus.Delivered.

Wrap up

This is something that Visual Studio will not currently alert you to unless you have the JetBrains ReSharper plugin installed. Fortunately, I was writing code like this using JetBrains Rider which flagged this issue with the warning of

The pattern is redundant, it does not produce any runtime checks

which led me to the description and solution for this problem in the JetBrains Rider documentation.