Using C# 9 Records for Immutable Data Modeling
Learn how C# 9 records provide immutable data carriers with value‑based equality and concise with‑expressions, plus practical guidance on usage, trade‑offs, and verification steps.
16 Jul 2026, 06:31 UTC

Problem: accidental mutation when sharing data
When you pass objects between layers of an application, a mutable class can be changed unintentionally, leading to bugs that are hard to trace. Writing immutable classes by hand adds boilerplate (private setters, constructors, equality overrides) and still leaves room for mistakes.
Thesis: C# 9 records give you immutable data carriers with value‑based equality and a concise way to create updated copies
Records are a language feature introduced in C# 9 (.NET 5+). They generate immutable properties, equality members, and a with expression that produces a new instance while re‑using unchanged fields.
Defining a record with positional syntax
You declare a record much like a class, but the parameters in the header become public init‑only properties.
public record Person(string FirstName, string LastName, int Age);
The compiler creates FirstName, LastName, and Age get‑only properties (init‑only in the constructor) and a primary constructor that matches the parameter list.
Value‑based equality and deconstruction
Two record instances are considered equal when all their properties match, regardless of reference. Records also support deconstruction, letting you split a record into variables.
var alice = new Person("Alice", "Smith", 30);
var bob = new Person("Bob", "Jones", 25);
bool same = alice.Equals(bob); // false – different values
var (first, last, age) = alice; // deconstruction
Equality is based on the runtime type; a derived record will not be equal to a base‑type record even if the property values match.
With‑expressions for non‑destructive updates
When you need a copy with one or more properties changed, the with expression creates a new instance while sharing the unchanged fields.
var olderAlice = alice with { Age = 31 };
// olderAlice is a new Person instance; alice remains unchanged
Worked example: modeling a simple user profile
Create a console project, add the record, and experiment with equality and copying.
Open a terminal with write access to the folder where you want the project.
Run the following command (requires .NET 5 SDK or later):
dotnet new console -n UserProfileDemoThis scaffolds a new console application. If a folder named
UserProfileDemoalready exists, the command will fail – choose a different name or delete/rename the existing folder.Navigate into the project folder and replace the generated
Program.cswith the code below:using System; public record UserProfile(string Email, string DisplayName, bool IsActive); class Program { static void Main() { var original = new UserProfile("alice@example.com", "Alice", true); var updated = original with { DisplayName = "Alicia" }; Console.WriteLine("Original:" + original); Console.WriteLine("Updated :" + updated); Console.WriteLine("Are they equal? " + original.Equals(updated)); Console.WriteLine("Same reference? " + ReferenceEquals(original, updated)); } }Build and run the project:
dotnet runThe program will display the string representation of both instances (generated by the compiler‑provided
ToString), show that the two objects are not equal because theDisplayNamediffers, and confirm they are different references.
Trade‑offs and limitations
Allocation overhead: Each
withexpression allocates a new object. In tight loops or high‑frequency scenarios this can add pressure on the garbage collector. Considerstruct record(also C# 9) or a mutable class with careful copying when performance is critical.Inheritance equality: Equality checks compare the runtime type, so
BaseRecordandDerivedRecordwith identical property values are not equal. If you need polymorphic equality, you must overrideEqualsyourself.JSON serialization: Default serializers like
System.Text.Jsontreat records like classes. Polymorphic record hierarchies may need custom converters or attributes to preserve type information.
Actionable closing
Records are a pragmatic choice when you need immutable data transfer objects, dictionary keys, or simple domain models where value‑based equality simplifies comparisons. Start by targeting .NET 5 or later, use the positional syntax for clarity, and profile allocation impact if you notice many with expressions in hot paths. For high‑throughput scenarios, evaluate struct record or immutable structs as alternatives.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.