With each new release of C#, pattern matching improves for C# developers. While developers do not widely use pattern matching today, we’ll likely see its adoption grow as more teams and applications move from the Full Framework (.NET 4.8) to the runtime’s newer variants (.NET 5+). Like most unique features to the language, there’s always an initial knee-jerk reaction to either embrace it, use it everywhere, or outright hate it. Regardless of what camp we fall under, we must understand some everyday use cases in the chance we want to work with C# pattern matching.

This post will show some examples of pattern matching scenarios that I find helpful and that you, the reader, might want to consider using in your current or future projects.

The Working Types

For this guide, we’ll be using the following record types.

abstract record Food;
record Pizza(params string[] Toppings) : Food;
record Burger(int NumberOfPatties = 1, bool HasCheese = true): Food;
record Chicken(CookKind Kind = CookKind.Fried): Food;

enum CookKind
{
    Fried,
    Grilled,
    Baked
}

The Type Check And Declaration Combination

One of my favorite new uses for pattern matching involves an old keyword, if, and a new keyword, is, applied in a new and exciting way. Let’s take a look at some examples.

Food food = new Pizza("pepperoni");

// check and declare a variable of a specific type
if (food is Pizza pizza)
{
    Console.WriteLine($"this pizza has {string.Join(", ", pizza.Toppings)}");
}

We’re able to check if the variable meets a condition for a type and declare a variable for us to use within the scope of our if statement. If developers use any pattern matching in their code, then let it be this one.

Null Checking

Nullability is a newer feature to C# that hopes to help reduce null checks throughout our codebase by providing null safety guarantees. Most codebases haven’t enabled the feature yet, so many of us still have to rely on checking for null values. We’ll use the is keyword again and see how we can check whether our variable is null or is not null.

// check the variable is null
if (food is null)
{
    Console.WriteLine("It's Null!");
}

// check that the variable is something
if (food is object)
{
    
}

// same as "is object"
// but uses the object pattern
if (food is { })
{
    Console.WriteLine("Not Null!");
}

Developers seeing this for the first time would rightfully ask, why is this better than == of !=? It’s not better; it is just a different approach hoping to make code more human-readable. The hard truth is, the definition of human-readable depends on the human and is typically subjective.

Refining Exception Handling

My first experience with pattern matching was using the when keyword with Exception handling. Legacy libraries are notorious for throwing general exceptions with more details found in an InnerException or within aMessage property.

// refined exception handling
try
{
    // super complex legacy library
    // that throws one type of exception
}
catch (Exception e) 
    when (e.Message.Contains("Ah!"))
{
    // handle that strange exception
}

In this example, we’ll only catch an Exception that has a particular message. The code will throw other exceptions, allowing us to handle them later in our codebase.

Switch Statements

Switch statements get the most improved award when it comes to the addition of pattern matching. No longer do we have to settle for switch statements on primitive types. Now we are capable of using complex objects with the ability to nest more switch statements.

var healthy = food switch
{
    Pizza p => false,
    Burger b => false,
    // branching into a sub pattern matching
    Chicken c => c switch
    {
        { Kind: CookKind.Fried } => false,
        _ => true
    }, 
    _ => false
};

The essential characteristic to note in this code is that .NET will evaluate each entry in our switch statement in the order it is defined. Thinking of these switch statements as a train stops along a track can be helpful, with the _, the discard variable, being the last stop on the line.

We can also see examples of declaring types for Pizza, Burger, and Chicken. Under the Chicken switch statement, we use object pattern matching to determine the chicken kind.

Object Pattern Matching

With C# 9, developers received object pattern matching and several new keywords like and and or as part of their pattern matching toolbox. Deconstruction, a default feature of record types, adds another pattern matching pattern, as we’ll see in the sample code below.

food = new Burger(3, true);
// terse if statements
if (food is Burger {NumberOfPatties: > 2, HasCheese: true})
{
    Console.WriteLine("Yum!");
}

// use the deconstruct method of
// our Burger record type to make for a
// terser if statment
if (food is Burger(> 2, true))
{
    Console.WriteLine("Yum!");
}

// filter by type and declare a variable
Food lunch = new Chicken(CookKind.Grilled);
if (food is Burger { NumberOfPatties: > 0 and < 3 } burger)
{
    Console.WriteLine($"{burger.NumberOfPatties} Patties For Me!");
}

if (lunch is Chicken {Kind: CookKind.Baked or CookKind.Grilled} chicken)
{
    Console.WriteLine("Eating Healthy!");
}

These examples show how keywords like and and or can reduce our logical statements’ noise. We also see the variable declaration’s reappearance by adding a variable name to the end of our object pattern matching.

Conclusion

Pattern matching is a controversial topic in the .NET community, some arguing that it doesn’t “feel like C# anymore”, some arguing that functional languages “do it better”. Regardless of the opinions, these features have landed, and it is a good idea to learn pattern matching. Is there a technique or approach with Pattern matching that I missed?

If so, let me know on Twitter at @buhakmeh, and I might update this post with your examples.

As always, thanks for reading.