Pattern matching in C#

We are using pattern matching in C# since the very beginning of C# through a combination of If/Else and Switch/Case statements. Pattern Matching is when we check an object’s member variable or property to have a full or partial match to a sequence. But these forms of pattern matching is very limited.

pattern matching in C#

Functional programming languages like F#, on the other hand, are advanced supporting pattern matching since the inception of the language. We can do a lot more complex matches on an object rather than just a simple value comparison.

From C# 7 onwards, complex pattern matching is introduced in Switch/Case statements. Due to which, lot more opportunities to write easy to understand comparison code.

Pattern Matching in C# 7

Traditionally, we can use Switch/Case for only comparing a single sequence with a string value. And it has to be a proper match, meaning, there is no way to use a Like or Contains statement in the match.

using System;

namespace PatternMatch.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            switch (args[0])
            {
                case "Print":
                    Console.WriteLine("Hello World!");
                    break;
                case "Escape":
                    break;
                default:
                    break;
            }
        }
    }
}

With If/Else statement we can do that but to a limited extent of comparing a defined member variable with primitive data types only.

using System;

namespace PatternMatch.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args[0].Contains("Pri"))
            {
                Console.WriteLine("Hello World!");
            }
            else if (args[0].Contains("Es"))
            {
            }
            else
            {
            }
        }
    }
}

is pattern in C# 7

In C# 7, the pattern match got more advance. In the advanced pattern match, we can use even complex object in If/Else condition for matches using Is keyword. And now we can use is keyword for extraction of the object from the match and use it.

For example, if we want to match a base class Instrument to its subclasses and based on the type extracted we want to do something specific. In that case, we will use the is the keyword in conjunction with the If/Else condition.

namespace PatternMatch.Demo
{
    public class Instrument
    {
    }

    public class Stock : Instrument 
    {
        public string Volatility = "High";
    }

    public class Bond : Instrument 
    {
        public string Volatility = "Low";
    }

    public class Alternative : Instrument
    {
        public string Volatility = "Moderate";
    }
}
using System;

namespace PatternMatch.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(GetVolatility(new Stock()));
        }

        private static string GetVolatility(Instrument instrument)
        {
            if (instrument is Stock s)
                return s.Volatility;
            if (instrument is Bond b)
                return b.Volatility;
            if (instrument is Alternative a)
                return a.Volatility;
            return "No Match Found";
        }
    }
}

As you can see from the above example, how simple it becomes to use is keyword to extract and use a match. This simplifies the code significantly compared to when we had to do both is and as keywords to do the same thing.

when with Switch/Case

The next thing that C# 7 introduced is using keyword When along with a Switch/Case statement for pattern matching.

To demonstrate this feature with an example. Let’s make some changes to the above-created Instrument class hierarchy. Let’s now create two classes, SNP500IndexFund and EmergingMarketFund, both deriving from Stock. Now relative market volatility for EmergingMarketFund will be higher than SNP500IndexFund. And to demonstrate this, we can create a new GetVolatility method implementation.

namespace PatternMatch.Demo
{
    public class Instrument
    {
    }

    public class Stock : Instrument 
    {
        public string Volatility = "High";
    }

    public class SNP500IndexFund : Stock
    {
    }

    public class EmergingMarketFund : Stock 
    {
    }
}
using System;

namespace PatternMatch.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(GetVolatility(new Stock()));
        }

        private static string GetVolatility(Instrument instrument)
        {
            switch (instrument)
            {
                case Stock s when s is SNP500IndexFund:
                    return "Moderate";
                case Stock s when s is EmergingMarketFund:
                    return "High";
                default:
                    return "No Match Found";
            }
        }
    }

}

In the above example, we can directly SNP500IndexFund and EmergingMarketFund in the case statement. I used Stock instead just to demonstrate the use of When keyword.

Using var in Switch/Case

Using var declaration in Switch/Case works with traditional string comparison. For example, if we can to do some complex string comparison inside of Switch/Case, we can use var along with when keyword to accomplish the same.

For example, if we are comparing a string with multiple values, and during comparison, we want to trim the string first and then want to do a case insensitive comparison, we can do that using var and when keywords.

using System;

namespace PatternMatch.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(GetVolatility(args[0]));
        }

        private static int GetVolatility(string input)
        {
            switch (input)
            {
                case var s when s.Trim().ToLower() == "first":
                    return 0;
                case var s when s.Trim().ToLower() == "second":
                    return 1;
                default:
                    return -1;
            }
        }
    }
}

Note: The above example can be done using input.Trim().ToLower() in the switch itself, but I wanted to demonstrate how we can use var and when keyword along with string.

New pattern match features in C# 8

In my article https://dotnetcorecentral.com/blog/c-8-my-top-5-favorite-features-which-i-absolutely-love/ I have gone through one of the pattern match introduced in C# 8. But in this post, I am going to take a deep dive into every single feature that was introduced with C# 8 for pattern matching.

The core change in pattern matching for C# 8 is the introduction of a recursive pattern match. Meaning, the result of any pattern match expression itself is an expression. Enabling the output of one pattern expression to be an input to others.

The main improvements in C# 8 for pattern match are:

  • Improved switch expression
  • Property pattern match
  • Tuple pattern match
  • Positional pattern match

Let’s go through each one in detail with examples. In the examples, I have used very simple concepts to demonstrate each of the features. They are no way production-ready codes. These examples are only to demonstrate the features and to elaborate their usage.

Improved switch expression

Let’s consider an example to demonstrate the improvement in switch expression. For example, we will have an enum of error codes. And based in an error code we will return an integer number from a function GetErrorCode.

In the code below we will see how this is written in pre-C# 8.

namespace PatternMatch.Demo
{
    public enum ErrorCode
    {
        NotFound,
        InternalServerError,
        Unauthorised
    }

    public class ErrorHandler
    {
        public int GetErrorCode(ErrorCode errorCode)
        {
            switch (errorCode)
            {
                case ErrorCode.InternalServerError:
                    return 500;
                case ErrorCode.NotFound:
                    return 404;
                case ErrorCode.Unauthorised:
                    return 401;
                default:
                    return 500;
            }
        }
    }
}

The syntax change in C# 8 is below, and we can see how concise and functional the code looks.

namespace PatternMatch.Demo
{
    public enum ErrorCode
    {
        NotFound,
        InternalServerError,
        Unauthorised
    }

    public class ErrorHandler
    {
        public int GetErrorCode(ErrorCode errorCode) =>
            errorCode switch
            {
                ErrorCode.InternalServerError => 500,
                ErrorCode.NotFound => 404,
                ErrorCode.Unauthorised => 401,
                _ => 500
            };
    }
}

Now with the new syntax, we start with the name of the variable that we want to match on and then the switch keyword. And as you can see there is no case statements as well as return statement. The statement after the operator => will be returned. And the operator _ is equivalent to default, which is in line with functional programming languages like F#.

Property pattern match

Due to the support of the property pattern match, now we can match on a property of an object. This is a very handy feature, which can help write a complex pattern match very easily.

For demonstrating this feature, I am going to update the error code example above. And now instead of passing an enum to find the error codes, I will use a response object. And match on the ErrorCode property to get the error code.

namespace PatternMatch.Demo
{
    public enum ErrorCode
    {
        NotFound,
        InternalServerError,
        Unauthorised
    }

    public class Response
    {
        public ErrorCode ErrorCode { get; set; }
    }

    public class ErrorHandler
    {
        public int GetErrorCode(Response response) =>
            response switch
            {
                { ErrorCode: ErrorCode.InternalServerError } => 500,
                { ErrorCode: ErrorCode.NotFound } => 404,
                { ErrorCode: ErrorCode.Unauthorised } => 401,
                _ => 500
            };
    }
}

As you can see from the above example, the property and its match are separated by : and contained within a {}.

Tuple pattern match

The next topic discussion is a tuple pattern match. When we want to match on multiple values tuple pattern match is what we will use. For example, if we want to match on a combination of multiple values, we will use a tuple pattern match.

In the below example, we will assume that our Authorize function takes three parameters. An API key, an auth token and a boolean indicating if auth token is valid. We will return true only in case of API Key matches an appropriate value, or auth token is valid. For all other cases, we will return false.

namespace PatternMatch.Demo
{
    class Authorizer
    {
        public bool Authorize(string apiKey, string authToken, bool isValidToken) =>
            (apiKey, authToken, isValidToken) switch
            {
                ("12345", _, _) => true,
                (_,_,true) => true,
                (_,_,false) => false,
                _ => false
            };
        
    }
}

Positional pattern match

Finally, the last one to discuss is the positional pattern match. Positional pattern match uses deconstruct a feature introduced in C# 7. This is another feature that is inspired by functional programming language.

If an object provides a deconstruct method, we can use that object for a positional pattern match to match on the properties.

In the example, we will find the density of a city based on population. We will use a deconstruct to get tuple out of an object, and then pattern match on the object.

namespace PatternMatch.Demo
{
    class City
    {
        public string Name { get; }
        public int Area { get; }
        public double Population { get; }

        public City(string name, int area, double population) =>
            (Name, Area, Population) = (name, area, population);

        public void Deconstruct(out string name, out int area, out double population) =>
            (name, area, population) = (Name, Area, Population);
    }

    class CityRecomendation
    {
        string FindDensity(City city) =>
            city switch 
            { 
                var (name, area, population) when population > 100000 => "High",
                var (name, area, population) when population < 50000 => "Medium",
                var (name, area, population) when population < 25000 => "Low",
                _ => "Unknown"
            };
    }
}

As you can see in the above example, we are using var and a tuple to deconstruct the object, and then using when to match on a pattern.

Conclusion

In this post, I tried to cover all the possible cases of pattern matching in C# as of version 8.0. As you can see using these features can significantly simplify a lot of our code. Instead of using complex if/else and switch/case statements, we can use advance concise pattern match expressions to solve a lot of problems.

I would highly recommend going through the advanced pattern matching topics available in the Microsoft site. There are a lot of examples and samples which will help you with more ideas on how to use these features in production codes. https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/pattern-matching.

YouTube video of the code: https://youtu.be/oBvkbbdPTIk