C# Records vs Classes: When to Use Each and Why
When C# 9 introduced records, it changed how I model data in .NET. Instead of writing boilerplate Equals and GetHashCode overrides, I can declare a record in one line and get value equality, a clean ToString, and nondestructive mutation for free. This article explains what records are, how they differ from classes, and when to use each.
What is a record in C#?
A record is a special type introduced in C# 9 that tells the compiler to generate value-based equality, a formatted ToString, and support for with expressions automatically. You declare one with the record keyword.
The simplest form is the positional record, where you list the properties in the declaration line:
// Positional record - C# 9+
public record Person(string FirstName, string LastName);
// Equivalent to writing a class with:
// - public init-only properties FirstName and LastName
// - value-based Equals, GetHashCode, ==, !=
// - formatted ToString like: Person { FirstName = Grace, LastName = Hopper }
// - a Deconstruct methodYou can also use nominal syntax if you need more control over property declarations:
public record Person
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
}Both forms produce the same compiler-generated members. The positional form is more concise; the nominal form gives you more flexibility.
How does value equality work in records?
By default, classes compare by reference - two variables are only equal if they point to the same object in memory. Records compare by value - two record instances are equal if all their properties have equal values.
The compiler generates Equals, GetHashCode, ==, and != automatically for records, comparing every property.
// Class - reference equality
public class PersonClass
{
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
}
var c1 = new PersonClass { FirstName = "Grace", LastName = "Hopper" };
var c2 = new PersonClass { FirstName = "Grace", LastName = "Hopper" };
Console.WriteLine(c1 == c2); // False - different objects in memory
// Record - value equality
public record PersonRecord(string FirstName, string LastName);
var r1 = new PersonRecord("Grace", "Hopper");
var r2 = new PersonRecord("Grace", "Hopper");
Console.WriteLine(r1 == r2); // True - same property valuesThis makes records ideal for use as dictionary keys, in comparisons, and anywhere you care about the data inside the object rather than which object it is.
How do you modify a record with a with expression?
Because positional record class properties are init-only (you cannot set them after construction), you use a with expression to create a modified copy. The original record is never changed.
public record Person(string FirstName, string LastName);
var original = new Person("Grace", "Hopper");
var updated = original with { FirstName = "Margaret" };
Console.WriteLine(original); // Person { FirstName = Grace, LastName = Hopper }
Console.WriteLine(updated); // Person { FirstName = Margaret, LastName = Hopper }The with expression creates a shallow copy of the record and applies the specified changes. The original remains untouched. This is called nondestructive mutation.
For a property to be changeable in a with expression, it must have an init or set accessor. Positional record properties always have init, so they work automatically.
What is the difference between record class and record struct?
C# 10 added record struct. The type of record determines how it is stored in memory and how copying behaves:
| Feature | record / record class | record struct | readonly record struct |
|---|---|---|---|
| Introduced in | C# 9 | C# 10 | C# 10 |
| Memory | Heap (reference type) | Stack (value type) | Stack (value type) |
| Default equality | Value equality | Value equality | Value equality |
| Positional properties | init-only (immutable) | read-write | init-only (immutable) |
| Inheritance | Can inherit from record class | Not supported | Not supported |
| with expression | Yes | Yes | Yes |
// C# 10+ public record struct Point(int X, int Y); // mutable record struct public readonly record struct Size(int Width, int Height); // immutable record struct
Use record struct for small, self-contained value types where stack allocation matters. Use record class (or just record) for larger data objects or when you need inheritance.
Can records inherit from other types?
Record classes support inheritance from other record classes. There are important restrictions:
- A record class can inherit from another record class.
- A record class cannot inherit from a regular class.
- A regular class cannot inherit from a record class.
- Record structs cannot inherit from anything (structs never support inheritance).
public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);
var dog = new Dog("Rex", "Labrador");
Console.WriteLine(dog); // Dog { Name = Rex, Breed = Labrador }Value equality in derived records includes all properties from the base record. Two Dog instances are only equal if both Name and Breed match, and both are of type Dog at runtime.
List<string>), the reference itself cannot be reassigned after construction - but the contents of the list can still be changed. For deep immutability, use immutable collection types.When should you use records vs classes?
| Use records when... | Use classes when... |
|---|---|
| The type's primary purpose is storing data | The type has behavior and mutable state |
| Two instances with the same values should be equal | Identity matters - two objects with the same data are different things |
| You want immutability by default | You need to change properties after construction freely |
| You want auto-generated ToString for logging or debugging | You need custom ToString logic |
| You are modeling DTOs, API responses, domain value objects | You are modeling entities, services, or domain aggregates |
A good rule of thumb: if you find yourself writing new MyClass { ... } just to hold some data and pass it around, a record is probably the better fit.
Frequently asked questions
Record classes were introduced in C# 9 (released with .NET 5 in November 2020). Record structs and readonly record structs were added in C# 10 (released with .NET 6 in November 2021).
Positional record class properties are init-only by default, meaning they cannot be changed after the object is constructed. However, you can declare a record with mutable set properties if needed. Record structs are mutable by default - use readonly record struct for immutability.
A with expression creates a new record instance that is a shallow copy of an existing one, with one or more properties changed. The original is never modified. For example: var updated = original with { Name = "New Name" };
Yes. Because records implement value equality and override GetHashCode, two records with the same data produce the same hash code and are treated as equal keys in a dictionary.
They are identical. The record keyword on its own defines a reference type, which is the same as writing record class explicitly. The explicit record class form was introduced in C# 10 to make the distinction from record struct clearer.
No. A regular class cannot inherit from a record, and a record cannot inherit from a regular class. Records can only inherit from other records.