Test in production without watermarks.
Works wherever you need it to.
Get 30 days of fully functional product.
Have it up and running in minutes.
Full access to our support engineering team during your product trial
Pattern matching in C# is a powerful feature that was introduced in C# 7.0 and has since been expanded upon in subsequent versions. It allows developers to write more concise and expressive code when dealing with conditional statements, type checking, and deconstruction of objects.
Pattern matching expressions provide a flexible and intuitive way to match values against patterns and execute corresponding code blocks. In this article, we will explore the intricacies of pattern matching expressions in C#, including syntax, use cases, and code examples. At the end of the article, we will also explore a bit about IronPDF PDF Generation Library from Iron Software to generate a PDF document on the fly in C# applications.
Pattern matching in C# code presents a plethora of advantages:
Pattern matching is supported by the following expressions:
is
expressionswitch
statementsswitch
expressionsThe following patterns can be used to match with the constructs:
Declaration and type patterns are essential tools in C# for checking the compatibility of expression run-time types with given types. With declaration patterns, you can both check compatibility and declare a new local variable simultaneously. Consider the following example:
object greeting = "Iron Software is Awesome!";
if (greeting is string message)
{
Console.WriteLine(message.ToLower()); // output: iron software is awesome!
}
object greeting = "Iron Software is Awesome!";
if (greeting is string message)
{
Console.WriteLine(message.ToLower()); // output: iron software is awesome!
}
Here, the declaration pattern ensures that if the expression greeting
matches the type string
, it's assigned to the variable message
, enabling subsequent operations.
When any of the following conditions hold true, the Declaration Pattern holds good:
T
.T
, implements interface T
, or can be implicitly converted to T
.T
.T
.Consider the following example demonstrating the above conditions:
int? nullableX = 8;
int y = 45;
object boxedy = y;
if (nullableX is int a && boxedy is int b)
{
Console.WriteLine(a + b); // output: 53
}
int? nullableX = 8;
int y = 45;
object boxedy = y;
if (nullableX is int a && boxedy is int b)
{
Console.WriteLine(a + b); // output: 53
}
Here, nullableX
matches the pattern because it's a nullable value type with the underlying type int
, and boxedy
matches because it can be unboxed to int
.
When you only need to check the type of expression without declaring a new variable, you can utilize the discard _
, as seen in the example below:
public static decimal CalculateToll(Vehicle vehicle) => vehicle switch
{
Bus _ => 4.00m,
Motor _ => 8.50m,
null => throw new ArgumentNullException(nameof(vehicle)),
_ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
};
public static decimal CalculateToll(Vehicle vehicle) => vehicle switch
{
Bus _ => 4.00m,
Motor _ => 8.50m,
null => throw new ArgumentNullException(nameof(vehicle)),
_ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
};
In this snippet, the _
serves as a placeholder for any type matching Vehicle
.
Both declaration and type patterns ensure that expressions are non-null before pattern matching. You can check for non-null using a negated null constant pattern, as illustrated below:
if (inputVal is not null)
{
// ...
}
if (inputVal is not null)
{
// ...
}
This negation ensures that inputVal
is not null before proceeding with further operations.
By leveraging declaration and type patterns in your C# code, you can enhance readability, reduce lines of code, and express algorithms more effectively. These patterns provide a concise and expressive way to handle type-based logic and improve the maintainability of your codebase.
Constant patterns serve to verify if an expression result matches a specific constant value. Consider the following example:
public static decimal GetGroupTicketPrice(int visitorCount) => visitorCount switch
{
1 => 2.0m,
2 => 10.0m,
3 => 25.0m,
4 => 60.0m,
0 => 0.0m,
_ => throw new ArgumentException($"Not supported number of visitors: {visitorCount}", nameof(visitorCount)),
};
public static decimal GetGroupTicketPrice(int visitorCount) => visitorCount switch
{
1 => 2.0m,
2 => 10.0m,
3 => 25.0m,
4 => 60.0m,
0 => 0.0m,
_ => throw new ArgumentException($"Not supported number of visitors: {visitorCount}", nameof(visitorCount)),
};
Here, the constant patterns check if visitorCount
matches any of the specified constant values and return corresponding ticket prices.
In a constant pattern, you can employ various types of constant expressions, such as:
true
or false
).null
.An expression of type Span<char>
or ReadOnlySpan<char>
can match constant strings.
To check for null
, utilize a constant pattern like so:
if (inputVal is null)
{
return;
}
if (inputVal is null)
{
return;
}
Here, the pattern ensures that inputVal
is null before proceeding with further operations.
You can also use a negated null constant pattern to ascertain non-null values:
if (inputVal is not null)
{
// ...
}
if (inputVal is not null)
{
// ...
}
This pattern verifies that inputVal
is not null, allowing for subsequent operations to be performed safely.
By incorporating constant patterns into your C# code, you can effectively handle scenarios where specific constant values need to be matched, improving code clarity and maintainability.
Relational patterns provide a means to compare expression results with constants. Consider the following example:
Console.WriteLine(Classify(20)); // output: Too high
Console.WriteLine(Classify(double.NaN)); // output: Unknown
Console.WriteLine(Classify(4)); // output: Acceptable
static string Classify(double measurement) => measurement switch
{
< -4.0 => "Too low",
> 10.0 => "Too high",
double.NaN => "Unknown",
_ => "Acceptable",
};
Console.WriteLine(Classify(20)); // output: Too high
Console.WriteLine(Classify(double.NaN)); // output: Unknown
Console.WriteLine(Classify(4)); // output: Acceptable
static string Classify(double measurement) => measurement switch
{
< -4.0 => "Too low",
> 10.0 => "Too high",
double.NaN => "Unknown",
_ => "Acceptable",
};
Here, the relational patterns compare the measurement
against specific thresholds to determine its classification.
The Right-hand part of a relational pattern must be a constant expression, which can be of an integer, floating-point, char
, or enum
type. The <
, >
, <=
, >=
operators can be used on the left-hand side.
To match an expression result within a certain range, employ a conjunctive "and" pattern, as illustrated below:
Console.WriteLine(GetCalendarSeason(new DateTime(2024, 3, 12))); // output: spring
Console.WriteLine(GetCalendarSeason(new DateTime(2024, 7, 12))); // output: summer
Console.WriteLine(GetCalendarSeason(new DateTime(2024, 2, 12))); // output: winter
static string GetCalendarSeason(DateTime date) => date.Month switch
{
>= 3 and < 6 => "spring",
>= 6 and < 9 => "summer",
>= 9 and < 12 => "autumn",
12 or (>= 1 and <3) => "winter",
_ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};
Console.WriteLine(GetCalendarSeason(new DateTime(2024, 3, 12))); // output: spring
Console.WriteLine(GetCalendarSeason(new DateTime(2024, 7, 12))); // output: summer
Console.WriteLine(GetCalendarSeason(new DateTime(2024, 2, 12))); // output: winter
static string GetCalendarSeason(DateTime date) => date.Month switch
{
>= 3 and < 6 => "spring",
>= 6 and < 9 => "summer",
>= 9 and < 12 => "autumn",
12 or (>= 1 and <3) => "winter",
_ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};
This excerpt describes how the conjunctive "and" pattern is utilized to ascertain the calendar season based on the month falling within specific ranges. It also mentions that relational patterns provide a concise and expressive means to compare expression results against constants, thus enhancing code clarity and maintainability.
The discard pattern, denoted by _
, serves to match any expression, including null
. Take the following example:
Console.WriteLine(GetDiscountInPercent(DayOfWeek.Friday)); // output: 5.0
Console.WriteLine(GetDiscountInPercent(null)); // output: 0.0
Console.WriteLine(GetDiscountInPercent((DayOfWeek)10)); // output: 0.0
static decimal GetDiscountInPercent(DayOfWeek? dayOfWeek) => dayOfWeek switch
{
DayOfWeek.Monday => 0.5m,
DayOfWeek.Tuesday => 12.5m,
DayOfWeek.Wednesday => 7.5m,
DayOfWeek.Thursday => 12.5m,
DayOfWeek.Friday => 5.0m,
DayOfWeek.Saturday => 2.5m,
DayOfWeek.Sunday => 2.0m,
_ => 0.0m,
};
Console.WriteLine(GetDiscountInPercent(DayOfWeek.Friday)); // output: 5.0
Console.WriteLine(GetDiscountInPercent(null)); // output: 0.0
Console.WriteLine(GetDiscountInPercent((DayOfWeek)10)); // output: 0.0
static decimal GetDiscountInPercent(DayOfWeek? dayOfWeek) => dayOfWeek switch
{
DayOfWeek.Monday => 0.5m,
DayOfWeek.Tuesday => 12.5m,
DayOfWeek.Wednesday => 7.5m,
DayOfWeek.Thursday => 12.5m,
DayOfWeek.Friday => 5.0m,
DayOfWeek.Saturday => 2.5m,
DayOfWeek.Sunday => 2.0m,
_ => 0.0m,
};
In the discard pattern example above, it handles all the possible input values. All the days of the week are managed, and a default value is provided. With this, all possible values are handled. Discard pattern cannot be utilized as a pattern in an is
expression or a switch
statement. In such cases, a var
pattern can be used with a discard, like var _
, to match any expression. However, a discard pattern is permissible in a switch
expression. For further details, please refer to the Discard pattern section of the feature proposal note.
Logical patterns in C# offer powerful tools for pattern matching, including negation, conjunction, and disjunction, which allow for more flexible and expressive matching conditions.
not
pattern)The negation pattern, represented by not
, matches an expression when the negated pattern doesn't match the expression. This is particularly useful for checking if an expression is non-null, as demonstrated below:
if (input is not null)
{
// ...
}
if (input is not null)
{
// ...
}
Here, the code block is executed if input
is not null.
and
pattern)The conjunctive pattern, using the and
keyword, matches an expression when both patterns match the expression. This allows for combining multiple conditions, as illustrated by the following example:
Console.WriteLine(Classify(13)); // output: High
Console.WriteLine(Classify(-100)); // output: Too low
Console.WriteLine(Classify(5.7)); // output: Acceptable
static string Classify(double measurement) => measurement switch
{
< -40.0 => "Too low",
>= -40.0 and < 0 => "Low",
>= 0 and < 10.0 => "Acceptable",
>= 10.0 and < 20.0 => "High",
>= 20.0 => "Too high",
double.NaN => "Unknown",
};
Console.WriteLine(Classify(13)); // output: High
Console.WriteLine(Classify(-100)); // output: Too low
Console.WriteLine(Classify(5.7)); // output: Acceptable
static string Classify(double measurement) => measurement switch
{
< -40.0 => "Too low",
>= -40.0 and < 0 => "Low",
>= 0 and < 10.0 => "Acceptable",
>= 10.0 and < 20.0 => "High",
>= 20.0 => "Too high",
double.NaN => "Unknown",
};
In this example, the measurement
is classified based on its value range.
or
pattern)The disjunctive pattern, using the or
keyword, matches an expression when either pattern matches the expression. This allows for handling multiple possible conditions, as shown below:
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 1, 19))); // output: winter
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 10, 9))); // output: autumn
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 5, 11))); // output: spring
static string GetCalendarSeason(DateTime date) => date.Month switch
{
3 or 4 or 5 => "spring",
6 or 7 or 8 => "summer",
9 or 10 or 11 => "autumn",
12 or 1 or 2 => "winter",
_ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 1, 19))); // output: winter
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 10, 9))); // output: autumn
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 5, 11))); // output: spring
static string GetCalendarSeason(DateTime date) => date.Month switch
{
3 or 4 or 5 => "spring",
6 or 7 or 8 => "summer",
9 or 10 or 11 => "autumn",
12 or 1 or 2 => "winter",
_ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};
Here, the calendar season is determined based on the month of the provided date.
These pattern combinators can be repeatedly used to create more complex and precise matching conditions, enhancing the flexibility and readability of your code.
The property pattern enables the matching of an expression's properties or fields against nested patterns. An example of this can be seen in the following code snippet:
static bool IsConferenceDay(DateTime date) => date is { Year: 2020, Month: 5, Day: 19 or 20 or 21 };
static bool IsConferenceDay(DateTime date) => date is { Year: 2020, Month: 5, Day: 19 or 20 or 21 };
Here, the property pattern ensures that the provided date corresponds to one of the specified conference days.
You can also incorporate a runtime type check and variable declaration within a property pattern, as shown below:
static string TakeFive(object input) => input switch
{
string { Length: >= 5 } s => s.Substring(0, 5),
string s => s,
ICollection<char> { Count: >= 5 } symbols => new string(symbols.Take(5).ToArray()),
ICollection<char> symbols => new string(symbols.ToArray()),
null => throw new ArgumentNullException(nameof(input)),
_ => throw new ArgumentException("Unsupported input type."),
};
static string TakeFive(object input) => input switch
{
string { Length: >= 5 } s => s.Substring(0, 5),
string s => s,
ICollection<char> { Count: >= 5 } symbols => new string(symbols.Take(5).ToArray()),
ICollection<char> symbols => new string(symbols.ToArray()),
null => throw new ArgumentNullException(nameof(input)),
_ => throw new ArgumentException("Unsupported input type."),
};
Here, the property pattern is used to handle strings and collections of characters, ensuring proper handling based on their properties.
In C#, the positional pattern allows for deconstructing an expression result and matching the resulting values against corresponding nested patterns. For instance:
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
static string Classify(Point point) => point switch
{
(0, 0) => "Origin",
(1, 0) => "Positive X basis end",
(0, 1) => "Positive Y basis end",
_ => "Just a point",
};
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
static string Classify(Point point) => point switch
{
(0, 0) => "Origin",
(1, 0) => "Positive X basis end",
(0, 1) => "Positive Y basis end",
_ => "Just a point",
};
In this example, the positional pattern is utilized to classify points based on their coordinates.
Furthermore, you can reference nested properties or fields within a property pattern, known as an extended property pattern, introduced in C# 10:
static bool IsAnyEndOnXAxis(Segment segment) =>
segment is { Start.Y: 0 } or { End.Y: 0 };
static bool IsAnyEndOnXAxis(Segment segment) =>
segment is { Start.Y: 0 } or { End.Y: 0 };
This feature enhances the flexibility of property patterns by allowing direct access to nested properties.
These patterns provide powerful mechanisms for handling complex data structures and improving the readability and expressiveness of your code.
Var Pattern allows you to match any type. This can be particularly useful for capturing intermediate results within Boolean expressions or when multiple checks are required in switch case guards.
Here's an example demonstrating the use of var pattern in a Boolean expression:
static bool IsAcceptable(int id, int absLimit) =>
SimulateDataFetch(id) is var results
&& results.Min() >= -absLimit
&& results.Max() <= absLimit;
static int [] SimulateDataFetch(int id)
{
var rand = new Random();
return Enumerable
.Range(start: 0, count: 5)
.Select(s => rand.Next(minValue: -10, maxValue: 11))
.ToArray();
}
static bool IsAcceptable(int id, int absLimit) =>
SimulateDataFetch(id) is var results
&& results.Min() >= -absLimit
&& results.Max() <= absLimit;
static int [] SimulateDataFetch(int id)
{
var rand = new Random();
return Enumerable
.Range(start: 0, count: 5)
.Select(s => rand.Next(minValue: -10, maxValue: 11))
.ToArray();
}
In this example, SimulateDataFetch
returns an array of integers, and the is var
pattern captures the result in the results
variable, allowing subsequent calculations based on its properties.
Additionally, var patterns can be utilized within switch expressions or statements for more concise and readable code. Here's an example using var pattern in switch case guards:
public record Point(int X, int Y);
static Point Transform(Point point) => point switch
{
var (x, y) when x < y => new Point(-x, y),
var (x, y) when x > y => new Point(x, -y),
var (x, y) => new Point(x, y),
};
static void TestTransform()
{
Console.WriteLine(Transform(new Point(1, 2))); // output: Point { X = -1, Y = 2 }
Console.WriteLine(Transform(new Point(5, 2))); // output: Point { X = 5, Y = -2 }
}
public record Point(int X, int Y);
static Point Transform(Point point) => point switch
{
var (x, y) when x < y => new Point(-x, y),
var (x, y) when x > y => new Point(x, -y),
var (x, y) => new Point(x, y),
};
static void TestTransform()
{
Console.WriteLine(Transform(new Point(1, 2))); // output: Point { X = -1, Y = 2 }
Console.WriteLine(Transform(new Point(5, 2))); // output: Point { X = 5, Y = -2 }
}
In this example, the var pattern (x, y)
captures the coordinates of the point, allowing different transformations based on their values.
In a var pattern, the type of the declared variable is inferred from the compile-time type of the expression being matched against the pattern.
The var pattern provides a convenient way to handle various scenarios where the specific type of expression is not known in advance, improving code clarity and flexibility.
IronPDF Document Rendering is a library from Iron Software which specializes in PDF document generation. To get started, the first thing is to install the library from the NuGet Package manager or from the Visual Studio Package Manager.
# To install from the NuGet Package Manager Console
Install-Package IronPdf
# To install from the NuGet Package Manager Console
Install-Package IronPdf
Below image shows how to install from Visual Studio Installation Guide.
In the below code, we shall see how to generate a simple PDF document:
using IronPdf;
namespace IronPatterns
{
class Program
{
static void Main()
{
Console.WriteLine("-----------Iron Software-------------");
var renderer = new ChromePdfRenderer(); // var pattern
var content = " <h1> Iron Software is Awesome </h1> Made with IronPDF!";
// Declaration Pattern
int? nullableX = 8;
int y = 45;
object boxedy = y;
content += "<p>Declaration Pattern</p>";
if (nullableX is int a && boxedy is int b)
{
Console.WriteLine(a + b); // output: 53
content += $"<p>Output: {(a + b)}</p>";
}
// Relational patterns
content += "<p>Relational patterns</p>";
var season1 = GetCalendarSeason(new DateTime(2024, 2, 25));
Console.WriteLine(season1);
content += $"<p>2024, 2, 25: {season1}</p>";
var season2 = GetCalendarSeason(new DateTime(2024, 5, 25));
Console.WriteLine(season2);
content += $"<p>2024, 5, 25: {season2}</p>";
var season3 = GetCalendarSeason(new DateTime(2024, 7, 25));
Console.WriteLine(season3);
content += $"<p>2024, 7, 25: {season3}</p>";
var pdf = renderer.RenderHtmlAsPdf(content);
pdf.SaveAs("output.pdf"); // Saves our PdfDocument object as a PDF
}
static string GetCalendarSeason(DateTime date) => date.Month switch
{
>= 3 and < 6 => "spring",
>= 6 and < 9 => "summer",
>= 9 and < 12 => "autumn",
12 or (>= 1 and < 3) => "winter",
_ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};
}
}
using IronPdf;
namespace IronPatterns
{
class Program
{
static void Main()
{
Console.WriteLine("-----------Iron Software-------------");
var renderer = new ChromePdfRenderer(); // var pattern
var content = " <h1> Iron Software is Awesome </h1> Made with IronPDF!";
// Declaration Pattern
int? nullableX = 8;
int y = 45;
object boxedy = y;
content += "<p>Declaration Pattern</p>";
if (nullableX is int a && boxedy is int b)
{
Console.WriteLine(a + b); // output: 53
content += $"<p>Output: {(a + b)}</p>";
}
// Relational patterns
content += "<p>Relational patterns</p>";
var season1 = GetCalendarSeason(new DateTime(2024, 2, 25));
Console.WriteLine(season1);
content += $"<p>2024, 2, 25: {season1}</p>";
var season2 = GetCalendarSeason(new DateTime(2024, 5, 25));
Console.WriteLine(season2);
content += $"<p>2024, 5, 25: {season2}</p>";
var season3 = GetCalendarSeason(new DateTime(2024, 7, 25));
Console.WriteLine(season3);
content += $"<p>2024, 7, 25: {season3}</p>";
var pdf = renderer.RenderHtmlAsPdf(content);
pdf.SaveAs("output.pdf"); // Saves our PdfDocument object as a PDF
}
static string GetCalendarSeason(DateTime date) => date.Month switch
{
>= 3 and < 6 => "spring",
>= 6 and < 9 => "summer",
>= 9 and < 12 => "autumn",
12 or (>= 1 and < 3) => "winter",
_ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};
}
}
Here we are using IronPDF's ChromePdfRenderer
class to save the HTML string into a PDF document. The output is saved to the "output.pdf" document.
IronPDF can be used with a trial license obtained from the IronPDF Licensing Page. Provide an Email Id to generate a license key to be delivered to your email.
"IronPDF.LicenseKey": "<Your Key>"
"IronPDF.LicenseKey": "<Your Key>"
Place the License key in the appsettings.json
file as shown above.
Pattern matching expressions in C# offer a powerful and flexible way to write conditional statements, type checks, and object deconstructions in a concise and readable manner. By leveraging pattern matching, developers can enhance the clarity and maintainability of their code while reducing boilerplate and redundancy. Whether it's type checking, switch statements, or deconstruction, pattern matching expressions provide a versatile toolset for tackling a wide range of programming tasks in C#.
In conclusion, mastering pattern matching expressions can greatly improve your C# programming skills, enabling you to write cleaner, more expressive code that is easier to understand and maintain. We've also covered IronPDF's HTML to PDF Generation abilities, which can be leveraged to generate PDF documents.
Pattern matching in C# is a feature introduced in C# 7.0 that allows developers to perform concise and expressive conditional checks, type checking, and object deconstruction.
Pattern matching improves code readability, reduces lines of code, enhances maintainability, and allows for more expressive algorithms by simplifying complex conditional logic.
C# supports pattern matching through 'is' expressions, 'switch' statements, and 'switch' expressions.
Declaration and type patterns check the compatibility of an expression's runtime type with a given type and allow for declaring a new local variable if the match is successful.
Constant patterns verify if an expression result matches a specific constant value, such as integers, strings, or enum values, and execute corresponding logic based on the match.
Relational patterns allow expression results to be compared with constants using relational operators like <, >, <=, and >=, facilitating concise range checks.
The discard pattern, denoted by '_', matches any expression, including null, and is useful for ignoring specific values in a switch expression.
Property patterns match an expression's properties or fields against nested patterns, enabling detailed checks of object structures.
IronPDF is a library from Iron Software that specializes in PDF document generation. It can be installed via NuGet and used to render HTML content as PDF documents in C# applications.
Pattern matching clarifies code logic, making it easier to maintain and debug by clearly delineating logic blocks and reducing code redundancy.