Skip to content

Intermediate · Inheritance, interfaces and records · topic 21 of 43 · 30 min

Records and pattern matching

Explanation

A record is a type made for holding data. One line like record Product(string Name, decimal Price); gives you properties, a constructor, value-based equality (two records with the same data are equal), a readable ToString and with-expressions for making changed copies. Pattern matching lets you test the shape of data: its type, property values and ranges, in if statements and switch expressions.

Why records?

Much of a business app is data moving around: an order request, a search result, a message on a queue. For such data you want: easy creation, no accidental changes, and "same data means equal". Writing that by hand in a class is long. A record (C# 9) gives it in one line.

csharp
var a = new Product("Pen", 10m);
var b = new Product("Pen", 10m);
Console.WriteLine(a);          // Product { Name = Pen, Price = 10 }
Console.WriteLine(a == b);     // True: same data
Console.WriteLine(a.Name);     // Pen
// a.Price = 12m;              // error: init-only property

public record Product(string Name, decimal Price);
This positional record creates Name and Price as init-only properties, a constructor, value equality and ToString.

Value equality versus reference equality

For normal classes, == asks "is this the same object?" (reference equality). For records, == asks "do they hold the same data?" (value equality). Two different record objects with equal properties are equal. This is exactly what you want for data, and it makes tests easier to write.

with-expressions: copy with changes

csharp
var pen = new Product("Pen", 10m);
var costlier = pen with { Price = 12m };
Console.WriteLine(pen.Price);       // 10 (unchanged)
Console.WriteLine(costlier.Price);  // 12
with makes a new record, copying all values except those you change. The original stays the same: this is non-destructive mutation.

Deconstruction

csharp
var (name, price) = new Product("Notebook", 45m);
Console.WriteLine($"{name} costs {price}");
Positional records can be split into variables.

record class and record struct

DeclarationKindNotes
record Product(...)Reference type (same as record class)Properties are init-only
record struct Point(int X, int Y)Value type (C# 10)Positional properties are read-write
readonly record struct Point(int X, int Y)Value typeProperties are init-only: fully immutable

Records can also have normal bodies with extra methods and properties, and can inherit from other records. Entities whose data changes over time and which have an identity, like a database row being edited, are usually better as classes.

Pattern matching

A pattern is a test of a value's shape. You have used some already: the relational patterns >= 90 in switch expressions. C# supports many kinds, and you can combine them.

PatternExampleMatches when
Typex is string sx is a string (and gives it the name s)
Constantx is null, code is "UPI"Equal to a constant
Relational>= 60Compared to a constant
Logical>= 18 and < 60, "Sat" or "Sun", not nullCombined with and, or, not
Property{ Age: >= 60, City: "Pune" }Properties match their patterns
PositionalPoint(0, 0)Deconstructed values match
List (C# 11)[1, .., 9]Array/list items match in order
csharp
Console.WriteLine(Concession(new Passenger("Kamala", 66, false)));   // 0.4

static decimal Concession(Passenger p) => p switch
{
    { IsDisabled: true } => 0.75m,
    { Age: < 5 } => 1.0m,
    { Age: >= 60 } => 0.4m,
    _ => 0m
};

public record Passenger(string Name, int Age, bool IsDisabled);
Property patterns check properties directly. The first matching arm wins, so order matters.
csharp
object input = 42;
string result = input switch
{
    int n when n < 0 => "negative number",
    int n => $"number {n}",
    string s => $"text of length {s.Length}",
    null => "nothing",
    _ => "something else"
};
Console.WriteLine(result);    // number 42

int[] digits = { 1, 5, 9 };
Console.WriteLine(digits is [1, .., 9]);   // True
when adds an extra condition to an arm. .. in a list pattern matches any number of items.

Common mistakes and fixes

  • Expecting records to be fully immutable: a record holding a List<T> still lets someone change the list's items. Use read-only collections for deep immutability.
  • Putting a broad arm before a specific one: { Age: >= 5 } before { Age: >= 60 } means seniors never reach their arm. The compiler often warns when an arm can never match.
  • Forgetting the _ arm: a non-exhaustive switch expression throws at run time for unmatched values.
  • Using record struct when you wanted immutability: positional record struct properties are writable; add readonly.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Records and pattern matching · 1/7

Records and patterns

In this lesson you will meet records, types made for data, and pattern matching.

Prefer a video by a teacher? Find free videos on “C# records with expression value equality pattern matching switch” on YouTube ↗

In real life: Classifying UPI transactions for a monthly spending summary

A personal finance app reads UPI transactions as records and uses pattern matching to place each one in a category. Because records compare by value, the app can also spot exact duplicate entries easily.

csharp
var txns = new[]
{
    new Txn("Swiggy", 420m, "food"),
    new Txn("IRCTC", 1850m, "travel"),
    new Txn("Landlord", 15000m, "rent"),
    new Txn("Swiggy", 420m, "food"),
};

foreach (var t in txns)
    Console.WriteLine($"{t.Payee,-9} {Label(t)}");
Console.WriteLine(txns[0] == txns[3] ? "Possible duplicate found" : "No duplicates");

static string Label(Txn t) => t switch
{
    { Tag: "rent" } => "Fixed cost",
    { Amount: > 1000m, Tag: "travel" } => "Big travel",
    { Tag: "food", Amount: < 500m } => "Small food order",
    _ => "Other"
};

public record Txn(string Payee, decimal Amount, string Tag);
Labels each row (Small food order, Big travel, Fixed cost, Small food order), then shows Possible duplicate found because two records hold identical data.

Practice (0/4 done)

Task 1 · Question

record City(string Name, int Pin); What does Console.WriteLine(new City("Agra", 282001) == new City("Agra", 282001)); print?

Task 2 · Question

var p = new Product("Pen", 10m); var q = p with { Price = 15m }; What is p.Price?

Task 3 · Question

int age = 30; string s = age switch { < 18 => "minor", >= 18 and < 60 => "adult", _ => "senior" }; What is s?

Task 4 · Practice

Write a record Order(string Id, decimal Amount, string City, bool IsPrime). Write a method DeliveryDays(Order o) using a switch expression with property patterns: Prime orders in "Mumbai" or "Delhi" take 1 day, other Prime orders 2 days, non-Prime orders over Rs 499 take 3 days, and everything else 5 days.

Quick quiz

Question 1

1.What does == compare for two record objects?

Question 2

2.What does new Product("Pen", 10m).ToString() give for record Product(string Name, decimal Price)?

Question 3

3.Is record struct a value type or a reference type?

Question 4

4.What does when do in a switch arm?

Question 5

5.int[] a = { 2, 4, 6, 8 }; What is a is [2, .., 8]?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does == compare for two record objects?

Answer: Their property values

Records generate value-based equality.

What does new Product("Pen", 10m).ToString() give for record Product(string Name, decimal Price)?

Answer: Product { Name = Pen, Price = 10 }

Records print their type name and properties.

Is record struct a value type or a reference type?

Answer: Value type

record struct (C# 10) is a value type.

What does when do in a switch arm?

Answer: Adds an extra condition the arm must also satisfy

when is a case guard.

int[] a = { 2, 4, 6, 8 }; What is a is [2, .., 8]?

Answer: True

The list pattern needs 2 first, 8 last, and anything in between.

What is the default value of a reference-type field?Related

Answer: null

Reference-type fields default to null.

From Structs, value types and reference types

Without overriding ToString, what does printing an object of class City show?Related

Answer: Its class name (like City)

The default ToString returns the type's full name.

From Classes, objects and constructors

Which is TRUE about records?Related

Answer: They generate equals, hashCode and toString

Records are immutable data carriers with generated methods.

From toString, equals, hashCode and records

Which makes a deep copy that keeps Dates as Dates?Related

Answer: structuredClone(obj)

structuredClone copies every level and supports Dates, Maps and Sets.

From References, copies and equality

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Inheritance, interfaces and records”. The module exam comes next.
  2. Module 5 exam: Inheritance, interfaces and records · 13 questions · 20 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Collections, generics and LINQ. 43 topics left in the course before the final exam.

← Previous: Interfaces: contracts for behaviour