C# 8 - Pattern Matching, Indices And Ranges

Patterns are criteria which can be used to test if a value matches the desired pattern. Prior to C# 8, we already had patterns, one example of a  pattern is a switch statement where we use cases to match with.
 
We’ll discuss Position Pattern, Property Pattern, Switch Pattern, Tuple Pattern, Ranges and Indices with examples.
 
Let's consider our entities for Customer, Address, Order and Product like below, these are simple modules to work with,
  1. public class Customer    
  2.    {    
  3.        public Guid Id { getset; }    
  4.        public string FirstName { getset; }    
  5.        public string LastName { getset; }    
  6.        public string FullName    
  7.        {    
  8.            get { return $"{FirstName} {LastName}"; }    
  9.        }    
  10.     
  11.        public bool PurchaseHistory { getset; }    
  12.     
  13.        public string Email { getset; }    
  14.        public Address CustomerAddress { getset; }    
  15.     
  16. public Customer(Guid id, string firstname, string lastname, string email, bool purchaseHistory, Address address)  
  17. {  
  18. Id = id;  
  19. FirstName = firstname;  
  20. LastName = lastname;  
  21. Email = email;  
  22. PurchaseHistory = purchaseHistory;  
  23. CustomerAddress = address;  
  24. }  
  25.      
  26.          
  27.    }    
  28.     
  29. public class Address    
  30.    {    
  31.        public int PostalCode { getset; }    
  32.        public string Street { getset; }    
  33.        public string Country { getset; }    
  34.          
  35. public Address(int postalCode, string street, string country)  
  36. {  
  37. PostalCode = postalCode;  
  38. Street = street;  
  39. Country = country;  
  40. }  
  41.          
  42.    }    
  43.     
  44. public class Order    
  45.    {    
  46.        public int Number { getset; }    
  47.     
  48.        public decimal Amount { getset; }    
  49.     
  50.        public PaymentMethods PaymentMethod { getset; }    
  51.     
  52.        public Customer Customer { getset; }    
  53.        public List<Product> LineItems { getset; }    
  54.     
  55.          
  56.    }    
  57. public class Product    
  58.    {    
  59.        public Guid Id { getset; }    
  60.     
  61.        public string Name { getset; }    
  62.     
  63.        public decimal Price { getset; }    
  64.     
  65.     
  66.    }    

Position Patterns

 
The feature is inspired from functional programming & uses de-constructor . The deconstructor should be a public method named as Deconstructor. It has out parameters for values that need to deconstruct. Let' say, we need a function which determines if a customer is eligible for free shipping. Criteria to determine free shipping is; if a customer is from Finland. We need to create deconstructor in Customer and Address classes.  
  1. //Deconstruct for Customer  
  2. public void Deconstruct(out Guid id, out string firstname, out string lastname, out string email, out bool purchaseHistory, out Address address)  
  3. {  
  4.     id = Id;  
  5.     firstname = FirstName;  
  6.     lastname = LastName;  
  7.     email = Email;  
  8.     purchaseHistory = PurchaseHistory;  
  9.     address = CustomerAddress;  
  10. }   
  1. // Deconstruct for Address  
  2.       public void Deconstruct(out int postalCode, out string street, out string country)  
  3.         {  
  4.             postalCode = PostalCode;  
  5.             street = Street;  
  6.             country = Country;  
  7.   
  8.         }  
Now, we can create our function that can match parameter as position; (_,_,"Finland") in code block below is Address class. The function is returning bool value if Address's third parameter is "Finland".
  1. public static bool IsFreeShippingEligible(Customer customer)  
  2.       {  
  3.           return customer is Customer(_,_ ,_ , _ , true, (_,_,"Finland") ); //  If custoemr has already a purchase and from Finland then Free shipping applies.  
  4.       }  

Property Patterns

 
The property pattern, as the name says, supports matching on property of object. It can be a good choice when dealing with complex pattern matching. It is easier to read and maintain code. Let's write a function to determine if a customer is eligible for discount. The criteria to decide discount is, customer has a purchase history. 
  1. public static bool IsDiscountEligible(Customer customer)  
  2.     {  
  3.         return customer is { PurchaseHistory: true, CustomerAddress: { Country: "Finland" }  }; // If customer has already  a purchase & from Finland then  discount applies  
  4.     }  
IsDiscountEligible is returning bool value if PurchaseHistory is true and Country is Finland.
 

Switch Patterns

 
It helps to use switch expression syntax with fewer case, break and curly braces so basically it's a very handy code. Let's write a function that can give us a live business feed; i.e. when there is a new lead or an order is received. 
  1. public static string LiveBusinessFeed(object business)  
  2.    {  
  3.        return  business switch  
  4.        {  
  5.            Customer c => $"New Lead: {c.FullName} with email {c.Email}",  
  6.            Order o => o  switch  
  7.            {  
  8.                _ when o.Amount > 5000 => $"Big sale of {o.Amount} -  some drinks & snackes in kitchen :)",  
  9.                _ => $"New sale : {o.Amount} by {o.Customer.Email}"  
  10.            },  
  11.   
  12.            _ => "Our team is trying :) " // unknown or Default  
  13.        };  
  14.          
  15.    }  
In the above function, object parameter can be Customer or Order, based on type, we are sending message as feed.
 

Tuple Patterns

 
The complex logic can be written in very clean code like below. When we have multiple inputs and we need to match by combination of inputs then Tuple Pattern is useful.
 
Let's write a function that determines discount based on order,
  • if order is paid by credit card and country is Finland then return 5 to apply 5 percentage discounts.
  • if order is paid by Wire Transfer and country is US then return 2 to apply 2 percent of discount
  • if order amount is 5000 or more then return 10 to apply 10 percent discount
  • otherwise return 0 for 0 percentage
  1. public static int GetOrderDiscount(Order order)  
  2.  {  
  3.      return (order.PaymentMethod,order.Customer.CustomerAddress.Country) switch  
  4.      {  
  5.          (PaymentMethods.CreditCard,"Finland" ) => 5,  
  6.          (PaymentMethods.WireTransfer, "US") => 2,  
  7.          (_,_) when order.Amount > 5000 => 10,  
  8.          _ => 0 // unknown or Default  
  9.      };  
  10.   
  11.  }  
The return is a number which is dicount calculated on different inputs.
 

Indices & Ranges

 
We have one string array with few words like below,
  1. var words = new string[]  
  2.             {  
  3.                 "This",  
  4.                 "is",  
  5.                 "C# 8",  
  6.                 "features",  
  7.                 "demo",  
  8.                 "example"  
  9.             };  
New range operator is like two dots .. . We can use AsSpan method from C# 7 but new features make code clean and more readable. If we need to print "This is C# 8" only then we can use range like below:
  1. Range seq = 0..3;  
  2. var sentence = words[seq];  
  3. Array.ForEach(sentence, word => Console.Write($"{word} "));  
 This gives us words from index 0 till element 3rd.
 
C# 8 - Pattern Matching, Indices And Ranges
 
If we change the range 0..6 then 6 is the length of array words. The console prints: 
 
C# 8 - Pattern Matching, Indices And Ranges
 
Index in C# 8 are very clean to write. We can use hat(^) operator to select index from the end.
  1. var lastThree = words[^3..];  
  2.          Array.ForEach(lastThree, word => Console.Write($"{word} "));  
 C# 8 - Pattern Matching, Indices And Ranges
 
The words[^3..] selects index of 3rd element from the end and range until the  end by using double dots. The sequence in index is zero based but to select the  last element from the end, we need to write code like below:
  1. var last = words[^1];  
  2. Console.Write($"{last} ");  
C# 8 - Pattern Matching, Indices And Ranges
 
It's worth it to mention that the last element cannot be accessed by ^0, application can throw IndexOutOfRangeException exception:
 
C# 8 - Pattern Matching, Indices And Ranges
 
To display "C# 8 features demo example", we can use hat(^) operator and range together like this:
  1. Console.WriteLine("From middle words till end:");  
  2.             var middle = words[2..^0];  
  3.             Array.ForEach(middle, word => Console.Write($"{word} "));  
  4.               
C# 8 - Pattern Matching, Indices And Ranges