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,
- public class Customer
- {
- public Guid Id { get; set; }
- public string FirstName { get; set; }
- public string LastName { get; set; }
- public string FullName
- {
- get { return $"{FirstName} {LastName}"; }
- }
-
- public bool PurchaseHistory { get; set; }
-
- public string Email { get; set; }
- public Address CustomerAddress { get; set; }
-
- public Customer(Guid id, string firstname, string lastname, string email, bool purchaseHistory, Address address)
- {
- Id = id;
- FirstName = firstname;
- LastName = lastname;
- Email = email;
- PurchaseHistory = purchaseHistory;
- CustomerAddress = address;
- }
-
-
- }
-
- public class Address
- {
- public int PostalCode { get; set; }
- public string Street { get; set; }
- public string Country { get; set; }
-
- public Address(int postalCode, string street, string country)
- {
- PostalCode = postalCode;
- Street = street;
- Country = country;
- }
-
- }
-
- public class Order
- {
- public int Number { get; set; }
-
- public decimal Amount { get; set; }
-
- public PaymentMethods PaymentMethod { get; set; }
-
- public Customer Customer { get; set; }
- public List<Product> LineItems { get; set; }
-
-
- }
- public class Product
- {
- public Guid Id { get; set; }
-
- public string Name { get; set; }
-
- public decimal Price { get; set; }
-
-
- }
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.
-
- public void Deconstruct(out Guid id, out string firstname, out string lastname, out string email, out bool purchaseHistory, out Address address)
- {
- id = Id;
- firstname = FirstName;
- lastname = LastName;
- email = Email;
- purchaseHistory = PurchaseHistory;
- address = CustomerAddress;
- }
-
- public void Deconstruct(out int postalCode, out string street, out string country)
- {
- postalCode = PostalCode;
- street = Street;
- country = Country;
-
- }
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".
- public static bool IsFreeShippingEligible(Customer customer)
- {
- return customer is Customer(_,_ ,_ , _ , true, (_,_,"Finland") );
- }
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.
- public static bool IsDiscountEligible(Customer customer)
- {
- return customer is { PurchaseHistory: true, CustomerAddress: { Country: "Finland" } };
- }
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.
- public static string LiveBusinessFeed(object business)
- {
- return business switch
- {
- Customer c => $"New Lead: {c.FullName} with email {c.Email}",
- Order o => o switch
- {
- _ when o.Amount > 5000 => $"Big sale of {o.Amount} - some drinks & snackes in kitchen :)",
- _ => $"New sale : {o.Amount} by {o.Customer.Email}"
- },
-
- _ => "Our team is trying :) "
- };
-
- }
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
- public static int GetOrderDiscount(Order order)
- {
- return (order.PaymentMethod,order.Customer.CustomerAddress.Country) switch
- {
- (PaymentMethods.CreditCard,"Finland" ) => 5,
- (PaymentMethods.WireTransfer, "US") => 2,
- (_,_) when order.Amount > 5000 => 10,
- _ => 0
- };
-
- }
The return is a number which is dicount calculated on different inputs.
Indices & Ranges
We have one string array with few words like below,
- var words = new string[]
- {
- "This",
- "is",
- "C# 8",
- "features",
- "demo",
- "example"
- };
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:
- Range seq = 0..3;
- var sentence = words[seq];
- Array.ForEach(sentence, word => Console.Write($"{word} "));
This gives us words from index 0 till element 3rd.
If we change the range 0..6 then 6 is the length of array words. The console prints:
Index in C# 8 are very clean to write. We can use hat(^) operator to select index from the end.
- var lastThree = words[^3..];
- Array.ForEach(lastThree, word => Console.Write($"{word} "));
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:
- var last = words[^1];
- Console.Write($"{last} ");
It's worth it to mention that the last element cannot be accessed by ^0, application can throw IndexOutOfRangeException exception:
To display "C# 8 features demo example", we can use hat(^) operator and range together like this:
- Console.WriteLine("From middle words till end:");
- var middle = words[2..^0];
- Array.ForEach(middle, word => Console.Write($"{word} "));
-