Record Types in C#
Vaibhav • September 11, 2025
In previous chapters, we’ve explored how to define classes, create methods, and organize data using fields and properties. But as your programs grow, you’ll often need simple data containers - types that hold values, support equality checks, and are easy to use in collections. That’s where record types come in.
Introduced in C# 9.0, record types are a concise way to define immutable reference types with built-in value equality. They’re perfect for modeling data - like configuration settings, DTOs, or results from a computation - where the identity of the object is based on its contents, not its memory location.
What is a Record?
A record is a reference type that’s designed for storing data. Unlike regular classes, records automatically
implement value-based equality, provide a readable ToString()
, and support
immutability by default.
public record Person(string Name, int Age);
This single line defines a complete type with two properties, a constructor, equality logic, and more. It’s equivalent to writing a full class with boilerplate code - but much cleaner.
Records are reference types, just like classes. But they behave differently when it comes to equality and immutability.
Positional Records
The example above is a positional record. It defines properties and a constructor in one line. You can use it like this:
var p1 = new Person("Vaibhav", 30);
Console.WriteLine(p1.Name); // Vaibhav
Console.WriteLine(p1.Age); // 30
The compiler generates Name
and Age
as init
-only properties, meaning they can only be set during initialization.
Value-Based Equality
One of the biggest advantages of records is how they handle equality. With classes, two objects are equal only if they reference the same memory. With records, equality is based on property values:
var p1 = new Person("Vaibhav", 30);
var p2 = new Person("Vaibhav", 30);
Console.WriteLine(p1 == p2); // True
Console.WriteLine(p1.Equals(p2)); // True
This makes records ideal for scenarios where you care about the data, not the identity.
Use records for types that represent data - especially when you want to compare instances by value.
Immutability with Init-Only Properties
Records use init
-only properties by default. This means you can set them during
object creation, but not afterwards:
var p = new Person("Vaibhav", 30);
p.Name = "Vikas"; // ❌ Error: cannot assign to init-only property
This immutability helps prevent bugs and makes your code easier to reason about.
With-Expressions for Non-Destructive Mutation
If you want to create a modified copy of a record, use a with
-expression:
var p1 = new Person("Vaibhav", 30);
var p2 = p1 with { Age = 31 };
Console.WriteLine(p2); // Person { Name = Vaibhav, Age = 31 }
This creates a new record with the same values as p1
, except for the updated
Age
. The original remains unchanged.
Note: With-expressions only work with records. They’re a safe way to “modify” immutable data.
Customizing Records
You can define records with regular property syntax too - especially if you want more control:
public record Product
{
public string Name { get; init; }
public decimal Price { get; init; }
}
This gives you flexibility to add methods, validation, or custom logic while still benefiting from record features.
Inheritance with Records
Records support inheritance, but with some rules. You can derive one record from another:
public record Animal(string Species);
public record Dog(string Species, string Breed) : Animal(Species);
This lets you model hierarchies while preserving value semantics. However, you can’t mix records and classes in inheritance - a record must inherit from another record.
Deconstruction Support
Records support deconstruction out of the box. You can extract values like this:
var person = new Person("Vaibhav", 30);
var (name, age) = person;
Console.WriteLine(name); // Vaibhav
Console.WriteLine(age); // 30
This is useful when you want to unpack a record into individual variables.
ToString Override
Records automatically generate a readable ToString()
:
var p = new Person("Vaibhav", 30);
Console.WriteLine(p); // Person { Name = Vaibhav, Age = 30 }
This is great for debugging and logging. You can override it if needed, but the default is often good enough.
Records vs Classes
So when should you use a record instead of a class? Here’s a quick comparison:
// Class: identity-based, mutable
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
// Record: value-based, immutable
public record User(string Name, int Age);
Use classes when you need mutable state or identity-based behavior. Use records when you want value semantics and immutability.
Records can be used as keys in dictionaries and sets - because they implement value equality correctly.
Limitations and Considerations
Records are powerful, but they’re not always the right choice. Here are a few things to keep in mind:
- Records are reference types - they’re not structs.
- They’re best for immutable data. If you need mutability, consider using classes.
- Equality is based on public properties. Private fields are not considered.
- Mixing records and classes in inheritance is not allowed.
Summary
Record types offer a concise, expressive way to define data-centric types in C#. They support value-based
equality, immutability, readable output, and safe copying via with
-expressions.
You’ve learned how to define positional records, customize them with properties, use inheritance, deconstruct
values, and compare instances by content.
Records are ideal for modeling data - especially when you want clarity, safety, and simplicity. In the next article, we’ll explore Init-Only Properties - a feature that complements records and helps enforce immutability in your own types.