Simplify Immutable Data Modeling with C# 9 Record Types
Learn how C# 9 records cut boilerplate for immutable data models, with a concrete Point2D example, pattern‑matching tips, and guidance on trade‑offs.
02 Aug 2026, 15:33 UTC

The boilerplate burden of immutable classes
When you need an immutable data carrier, the classic approach is to write a class with a constructor, read‑only properties, overridden Equals and GetHashCode, and a sensible ToString. Even a simple two‑dimensional point quickly balloons to dozens of lines, and any change to the fields requires updating all those members. This repetition not only slows development but also opens the door to bugs when equality or string formatting falls out of sync.
Rewriting the same model as a record
C# 9 introduced the record keyword, which generates the boilerplate for you while giving the type reference‑type semantics and value‑based equality. The same Point2D becomes a single line:
public record Point2D(int X, int Y);
The compiler synthesizes a constructor, public init‑only properties, Equals/GetHashCode that compare each field, and a ToString that shows the property names and values. You can still add custom members if needed, but the core immaterial work is done.
Pattern‑matching and nondestructive updates
Records shine when you combine them with pattern matching and the with expression. Equality checks become intuitive:
var p1 = new Point2D(3, 4);
var p2 = new Point2D(3, 4);
Console.WriteLine(p1 == p2); // True – value based equality
Deconstruction lets you pull the fields into variables:
var (x, y) = p1;
Console.WriteLine($"{x}, {y}"); // 3, 4
When you need a modified copy, the with expression creates a new instance while leaving the original untouched:
var moved = p1 with { X = p1.X + 1 };
Console.WriteLine(moved); // Point2D { X = 4, Y = 4 }
Console.WriteLine(p1); // Point2D { X = 3, Y = 4 } (unchanged)
Trade‑offs and when to reach for a struct instead
Records are reference types, so each instance lives on the managed heap. For high‑frequency scenarios where a stack‑allocated struct would avoid allocation overhead, a record may be unsuitable. Additionally, records support inheritance, but a record can only inherit from another record, limiting the flexibility you get with classic classes. If you need true value‑type semantics (e.g., for interop or low‑latency paths), consider a readonly struct instead.
Actionable steps
- Install the .NET 6 SDK (or newer) and create a console project:
dotnet new console -n PointDemo. - Set the language version to at least C# 9 in the project file:
<LangVersion>9.0</LangVersion>. - Replace the generated
Program.cswith thePoint2Drecord example above, plus the equality, deconstruction, andwithsnippets. - Build and run:
dotnet buildthendotnet run. Observe the printed equality result and the with‑expression output. - Optionally add a unit‑test project (
dotnet new xunit) and write assertions that verify value‑based equality and thatwithdoes not mutate the original.
By adopting records for domain models, you cut boilerplate, gain reliable equality, and unlock expressive pattern matching—while keeping an eye on allocation costs when performance is critical.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.