C# Pattern matching

(all code is here)

Overview of scenarios where you can use pattern matching. These techniques may improve the readability and correctness of your code.

Switch statement vs switch expression

First of all we need to clear when should we use which one

Use switch statement (old) when:

  • you need to call void methods
  • you need to execute multiple tasks
  • the result is not a value, but they’re actions

Use switch expression (new) when:

  • you need to map one entry value to one exit value
  • you have a single call per case

    switch statement

    We have the following record

    public record Order(string Status, decimal Cost);
    

and the following void calls (they’re abstract just for the sake of the example as what they do is not important)

public abstract void UpdatePendingOrder(Order order);
public abstract void UpdateCancelledOrder(Order order);
public abstract void UpdateCompletedOrder(Order order);

Example on how to execute a call with secondary task

// DON'T DO THIS
public void NestedIfElseStatement(Order order)
{
	if (order.Status == "Pending")
	{
		UpdatePendingOrder(order);
		SendKpis(order);
	}
	else if (order.Status == "Completed")
	{
		UpdateCompletedOrder(order);
		SendKpis(order);
	}
	else if (order.Status == "Cancelled")
	{
		UpdateCancelledOrder(order);
		SendEmail(order);
	}
	else
	{
		throw new InvalidOperationException("Unknown status");
	}
}

As this processes each status executing multiple tasks we’d solve this with a switch statement

public void SwitchStatement(Order order)
{
	switch (order.Status)
	{
		case "Pending":
			UpdatePendingOrder(order);
			SendKpis(order);
			break;
		case "Completed":
			UpdateCompletedOrder(order);
			SendKpis(order);
			break;
		case "Cancelled":
			UpdateCancelledOrder(order);
			SendEmail(order);
			break;
		default:
			throw new InvalidOperationException("Unknown status");
	}
}

switch expression

We have the following record

public record Reference(int Id, bool Completed);

With the following methods

public abstract Reference GetPendingOrderRef(Order order);
public abstract Reference GetCancelledOrderRef(Order order);
public abstract Reference GetCompletedOrderRef(Order order);

Here we have a process where each status maps to exactly one execution and we have no secondary side effects.

// DON'T DO THIS
public Reference NestedIfElseExpression(Order order)
{
	if (order.Status == "Pending")
	{
		return GetPendingOrderRef(order);
	}
	else if (order.Status == "Completed")
	{
		return GetCompletedOrderRef(order);
	}
	else if (order.Status == "Cancelled")
	{
		return GetCancelledOrderRef(order);
	}
	else
	{
		throw new InvalidOperationException("Unknown status");
	}
}

this is the kind of case we may solve through switch expressions

public Reference SwitchExpression(Order order) =>
	order.Status switch
	{
		"Pending" => GetPendingOrderRef(order),
		"Completed" => GetCompletedOrderRef(order),
		"Cancelled" => GetCancelledOrderRef(order),
		_ => throw new InvalidOperationException("Unknown status"),
	};

_ is the discard pattern that matches all values. It handles any error conditions where the value doesn’t match one of the defined values.

check and map integer values

public string CheckReferenceId(Reference reference) =>
	reference.Id switch
	{
		<=100 => "system values",
		>100 and <=1000 => "test references",
		>1000 => "customer order",
	};

check and map multiple values at once

public string CheckMultipleValues(Reference reference) =>
	reference switch
	{
		{ Id: <= 100, Completed: false } => "system values - to do",
		{ Id: <= 100, Completed: true } => "system values - done",
		{ Id: >= 1000, Completed: false } => "customer orders - to ship",
		null => throw new ArgumentNullException(nameof(reference), "can't act on null"),
	};

check subclasses with multiple values

Now we have the following Order subclasses

public record Order(string Status, decimal Cost);
public record NAOrder(string Status, decimal Cost) : Order(Status, Cost);
public record EUOrder(string Status, decimal Cost) : Order(Status, Cost);

where we may check the subtype and their properties

public Reference CheckSpecialOrders(Order order) =>
	order switch
	{
		NAOrder o when o.Status == "Cancelled" && o.Cost > 10000 => GetCancelledOrderRef(order),
		EUOrder o when o.Status == "Cancelled" && o.Cost > 5000 => GetCancelledOrderRef(order),
		_ => throw new InvalidOperationException("Unknown status"),
	};

Apply pattern matching to if

With an if we may check doing this, instead of using multiple if, and, or

public void CheckCancelledOrders(Order order)
{        
	if (order is EUOrder { Status: "Cancelled", Cost: > 10000 })
	{
		// check and do GDPR compliant things
		SendEmail(order);
	}
	else if (order is NAOrder { Status: "Cancelled", Cost: > 10000 })
	{
		SendEmail(order);
	}
}

null checks

We may use pattern matching to check for null (instead of !=)

if (order is null)
	throw new InvalidOperationException("cannot operate on null");

list patterns

Since c# 11 we may check for list emptiness

if (orders is [])
	throw new ArgumentException("List of orders cannot be empty");

if (orders is not [])
	Console.WriteLine("list of orders is not empty");

Reference(s)

https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching
https://ianvink.wordpress.com/2021/12/11/part-1-c-10-pattern-matching-or-the-death-of-if-else/
https://dev.to/ahmedshahjr/improve-your-c-code-with-pattern-matching-5g7c
https://endjin.com/blog/2023/03/dotnet-csharp-11-pattern-matching-lists