On this page
Records and pattern matching in C# 14
What are records?
Records are reference types (or value types) designed to model immutable data. Unlike classes, records have:
- Value equality — two records with the same values are equal
- Automatic deconstruction
- A descriptive ToString() generated automatically
- with expression to clone with changes
// Class: equality by reference
class PersonClass { public string Name { get; set; } = ""; }
var c1 = new PersonClass { Name = "David" };
var c2 = new PersonClass { Name = "David" };
Console.WriteLine(c1 == c2); // false — different references
// Record: equality by value
record PersonRecord(string Name, int Age);
var r1 = new PersonRecord("David", 30);
var r2 = new PersonRecord("David", 30);
Console.WriteLine(r1 == r2); // true — same valuesPositional records
The most concise form. Primary constructor parameters become init-only properties:
// Positional syntax
record Coordinate(double Latitude, double Longitude);
var nyc = new Coordinate(40.71, -74.01);
Console.WriteLine(nyc); // Coordinate { Latitude = 40.71, Longitude = -74.01 }
// Deconstruction
var (lat, lon) = nyc;
Console.WriteLine($"Lat: {lat}, Lon: {lon}");
// LINQ with records
var cities = new List<Coordinate>
{
new(40.71, -74.01),
new(51.51, -0.13),
new(-33.87, 151.21),
};
var northernHemisphere = cities.Where(c => c.Latitude > 0);with expression
Creates a copy of the record with modified properties:
record Order(int Id, string Customer, decimal Total, string Status);
var order = new Order(1, "David", 299.99m, "pending");
// Change status without mutating the original
var approved = order with { Status = "approved" };
var cancelled = order with { Status = "cancelled", Total = 0m };
Console.WriteLine(order.Status); // pending — unchanged
Console.WriteLine(approved.Status); // approved
// Useful in transformation chains
var finalOrder = order
with { Status = "processing" }
with { Total = order.Total * 0.9m }; // 10% discountrecord struct
For small types copied frequently, record struct is more efficient as it lives on the stack:
// record struct — value semantics (copies, does not share reference)
record struct Color(byte R, byte G, byte B, byte A = 255)
{
public static readonly Color Red = new(255, 0, 0);
public static readonly Color Green = new(0, 255, 0);
public static readonly Color Blue = new(0, 0, 255);
public static readonly Color White = new(255, 255, 255);
// Instance method
public Color Blend(Color other) =>
new((byte)((R + other.R) / 2),
(byte)((G + other.G) / 2),
(byte)((B + other.B) / 2));
public string ToHex() => $"#{R:X2}{G:X2}{B:X2}";
}
var purple = Color.Red.Blend(Color.Blue);
Console.WriteLine(purple.ToHex()); // #7F007FPattern Matching
Pattern matching lets you check the shape of a value and extract information declaratively.
is expression
object obj = "Hello .NET";
// Type pattern
if (obj is string s)
Console.WriteLine($"It's a string with {s.Length} characters");
// Null check
if (obj is not null)
Console.WriteLine("Not null");
// Constant pattern
if (obj is "Hello .NET")
Console.WriteLine("Exact match");Advanced switch expression
// Property patterns
record Employee(string Name, string Role, decimal Salary, bool Active);
static decimal CalculateBonus(Employee emp) => emp switch
{
{ Active: false } => 0,
{ Role: "CEO" } => emp.Salary * 0.5m,
{ Role: "Manager", Salary: > 5000 } => emp.Salary * 0.2m,
{ Role: "Developer", Salary: > 3000 } => emp.Salary * 0.15m,
{ Active: true } => emp.Salary * 0.05m,
_ => 0
};
var dev = new Employee("David", "Developer", 4000m, true);
Console.WriteLine($"Bonus: ${CalculateBonus(dev):F2}"); // $600.00Relational and logical patterns
// Relational: <, >, <=, >=
static string ClassifyBMI(double bmi) => bmi switch
{
< 18.5 => "Underweight",
>= 18.5 and < 25.0 => "Normal",
>= 25.0 and < 30.0 => "Overweight",
>= 30.0 => "Obese"
};
// Logical: and, or, not
static bool IsBusinessHour(int hour) =>
hour is (>= 8 and <= 12) or (>= 14 and <= 18);
static bool IsWeekday(DayOfWeek day) =>
day is not (DayOfWeek.Saturday or DayOfWeek.Sunday);List patterns (C# 11+)
// Match lists by structure
static string AnalyzeSequence(int[] seq) => seq switch
{
[] => "empty",
[0] => "only zero",
[_, 0, _] => "zero in the middle",
[> 0, > 0, > 0] => "all positive (3 elements)",
[var a, var b] when a == b => $"two equal values: {a}",
[var h, .. var rest] => $"starts with {h}, {rest.Length} more"
};
Console.WriteLine(AnalyzeSequence(new[] { 5, 3, 8, 1 }));
// "starts with 5, 3 more"Deconstruct in patterns
record Point(double X, double Y);
static string LocatePoint(Point p) => p switch
{
(0, 0) => "Origin",
(> 0, > 0) => "Quadrant I",
(< 0, > 0) => "Quadrant II",
(< 0, < 0) => "Quadrant III",
(> 0, < 0) => "Quadrant IV",
(_, 0) or (0, _) => "On an axis"
};
var p = new Point(3, -1);
Console.WriteLine(LocatePoint(p)); // Quadrant IVPractice
- Domain records: Create
record Invoice(int Id, string Customer, List<InvoiceLine> Items)andrecord InvoiceLine(string Product, decimal Price, int Quantity). Usewithto mark an invoice as "paid". - Shape pattern matching: Implement
DescribeShape(object shape)that uses a switch expression with type patterns forCircle,Rectangle, andTriangle. - List patterns: Write a method that analyzes the first 3 elements of a price list and returns "Uptrend", "Downtrend", or "Stable".
In the next lesson we will start with ASP.NET Core 10: how to build web APIs with Minimal APIs and the middleware pipeline.
Records for DTOs and immutability
Use records for DTOs (Data Transfer Objects), Value Objects, and event messages. They are perfect when you want value equality and want to discourage mutation. The compiler automatically generates Equals, GetHashCode, ToString, and the == operator.
record vs record struct
record class lives on the heap (like normal classes). record struct lives on the stack (like structs) and is more efficient for small types that are copied frequently. Use record struct for coordinates, colors, ranges, and other small value types.
with does not mutate the original
The with expression always creates a NEW object with the specified changes. The original object remains intact. This is intentional: records are designed to be immutable, and immutability makes code easier to reason about.
// Record class — immutable by convention, value equality
public record Product(int Id, string Name, decimal Price);
// Positional: automatic deconstruction
var laptop = new Product(1, "Laptop Pro", 1299.99m);
var (id, name, price) = laptop;
Console.WriteLine($"#{id}: {name} — ${price:F2}");
// with expression — clone with changes (does NOT mutate original)
var discountedLaptop = laptop with { Price = 999.99m };
Console.WriteLine(laptop.Price); // 1299.99 — unchanged
Console.WriteLine(discountedLaptop.Price); // 999.99
// Value equality — comparison by value (not by reference)
var p1 = new Product(1, "Mouse", 29.99m);
var p2 = new Product(1, "Mouse", 29.99m);
Console.WriteLine(p1 == p2); // true (records)
Console.WriteLine(ReferenceEquals(p1, p2)); // false
// Record struct — value on stack, more memory-efficient
public record struct Point(double X, double Y);
var origin = new Point(0, 0);
var p = new Point(3.0, 4.0);
Console.WriteLine(p.ToString()); // Point { X = 3, Y = 4 }
// Record with additional properties and method
public record User(int Id, string Email)
{
public string Name { get; init; } = string.Empty;
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
public string Initial => Name.Length > 0 ? Name[0].ToString() : "?";
}
Sign in to track your progress