C# Records Explained: Why and How to Use Them
One of the most exciting features added to modern C# (starting with C# 9.0) is the record type. Records are designed to make your code more concise, readable, and immutable-friendly — perfect for modern application development.
If you’ve ever written a class just to hold data, overridden Equals
, GetHashCode
, and ToString
manually, or struggled with immutability, you’ll appreciate what records bring to the table.
In this article, we’ll cover everything you need to know about records in C#:
What a record is (and how it differs from a class)
Why records were added to the language
How to declare and use records
Value equality vs reference equality
Positional records and concise syntax
Inheritance and immutability with records
Real-world use cases
Performance considerations and trade-offs
Best practices for using records effectively
What Is a Record in C#?
A record is a special kind of reference type in C# designed to represent immutable data models. It’s similar to a class in many ways but with some important differences:
Records provide value-based equality by default.
Records emphasize immutability.
Records come with concise syntax for declaring properties.
Think of records as the “go-to” tool when you’re modeling data rather than behavior.
Classes vs Records: The Big Difference
Let’s start with a simple comparison.
Using a Class
A typical “Person” class
If you create two instances with the same values:
The equality operator compares object reference IDs by default- not the values of the object, as many people would guess
Even though the data is the same, they’re considered different objects.
Using a Record
Notice the “record” keyword, where you would normally use “class”
Now if you compare:
The records are comparing objects based their values
By default, records are compared by values of their properties — not by memory reference.
Why Records Were Added to C#
Before records, if you wanted this behavior with classes, you had to write boilerplate code:
Override
Equals()
Override
GetHashCode()
Override
ToString()
This was tedious and error-prone. Records give you all of that for free with one line of code.
Declaring Records
There are two main ways to declare records:
1. Positional Records (Concise Syntax)
Concise syntax for positional records
This:
Creates immutable properties (
Name
,Age
)Provides a constructor
Implements
Equals
,GetHashCode
, andToString
Example:
Using the Person record
2. Records with Property Declarations
A “Product” record with properties
This is closer to a traditional class style but still gets the benefits of records.
Init-Only Properties
Notice the use of init
instead of set
?
That means:
Note that the price cannot be modified after creation
Records encourage immutability: once created, the object’s data shouldn’t change.
Value Equality in Action
The key difference: records compare data, not references.
Comparison with records
Whereas with classes:
Class objects compare by object reference ID number by default, not considering 2 “people” the same just because they have the same name and age
With-Expressions
Another powerful feature of records: non-destructive mutation.
Using a “with-expression” to make a copy of a Person record, for. a new Person
This lets you “copy and modify” without changing the original object — super useful in immutable programming patterns.
Inheritance with Records
Records can inherit from other records:
Demonstrating inheritance with records
Example:
Creating a Dog record, derived from Animal
⚠️ However: records can only inherit from other records (not classes).
Real-World Use Cases for Records
1. DTOs (Data Transfer Objects)
When sending/receiving data via APIs, records are ideal for modeling payloads.
Example of a DTO record
2. Immutable State in Applications
Useful in functional or reactive programming. For example, in Redux-like state management systems:
Use records with functional programming
3. Pattern Matching
Records work beautifully with switch
expressions:
An example with a switch statement
Performance Considerations
Records are still reference types (like classes), so:
They live on the heap (not stack).
They’re garbage-collected.
They’re not as lightweight as
structs
.
If you need ultra-low overhead (e.g., high-performance gaming loops), use readonly struct
.
If you need value equality + immutability + conciseness, use record
.
Best Practices
Use records for immutable data models
DTOs, view models, messages, config objects.Don’t use records for mutable business objects
If you need setters and business logic, classes are still the right tool.Prefer positional records for simple models
Cleaner, more concise.Use
with
for copying/mutating safely
It makes code more readable and consistent.
Conclusion
Records in C# are a game-changer for data modeling. They combine conciseness, immutability, and value-based equality in a way that makes code simpler and safer.
You don’t need to write repetitive boilerplate for Equals
, GetHashCode
, or ToString
. You get immutable data patterns out of the box. And you can model data in a way that’s closer to functional programming while still leveraging all the power of C#.
Whenever you’re defining a type whose main purpose is holding data, ask yourself:
👉 “Should this be a record instead of a class?”
Chances are, the answer is yes.
Join us at our Skool community to keep the discussion going.