Records are needed when a type’s main purpose is to hold data and should behave by value (based on its contents) rather than by reference (based on its identity). They provide concise syntax for immutable data models, value-based equality, easy copying with modifications, and better default ToString output.
In contrast, classes are general-purpose reference types that use reference equality by default and are typically used for objects with identity and behavior, not just data.
Why use records
Use a record instead of a class or struct when:
- Value equality is desired
- Records compare equality by their contents: two record instances are equal if they are the same record type and all their fields/properties are equal.
- Classes use reference equality by default: two variables are equal only if they reference the same object instance.
- This is ideal for:
- Data transfer objects (DTOs)
- Configuration objects
- Results returned from services or queries
- Immutability is desired
- Records are primarily intended for immutable data models.
- Immutability is useful for:
- Thread safety
- Using objects as keys in dictionaries or in hash-based collections
- Functional-style programming where data is not mutated in place
- Concise, data-centric code is desired
- Records synthesize:
- Value-based
Equals / GetHashCode
- A useful
ToString that prints property names and values
- Support for nondestructive mutation via
with expressions
- This reduces boilerplate compared to hand-writing all of this in a class.
When not to use records
Records are not appropriate when:
- The type represents an entity whose identity is more important than its current field values (for example, ORM entities such as Entity Framework Core entities, which rely on reference equality and mutability).
- The type must be mutable and its identity should not change when its data changes.
Basic syntax
Record class (reference type):
public record Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
Positional record class (primary constructor syntax):
public record Person(string FirstName, string LastName);
Key points for record classes:
-
record or record class declares a reference type.
- Positional parameters in the primary constructor become public init-only properties.
- Equality is value-based: two
Person instances are equal if FirstName and LastName are equal.
-
ToString() prints something like:
Person { FirstName = Nancy, LastName = Davolio }
-
with expressions support nondestructive mutation:
var p1 = new Person("Nancy", "Davolio");
var p2 = p1 with { LastName = "Fuller" }; // p1 unchanged, p2 is a copy with one change
Record struct (value type):
public record struct Point(int X, int Y);
Key points for record structs:
-
record struct declares a value type.
- Positional properties are mutable in
record struct and immutable in readonly record struct.
- Compiler synthesizes equality and
ToString, and a Deconstruct method for positional record structs.
How records differ from classes and structs
Compared to classes:
- Primary constructor and positional parameters
- Records can use positional parameters in a primary constructor to define properties concisely and (for record classes) immutably.
- Equality semantics
- For classes:
== and Equals use reference equality by default.
- For records:
== and Equals use value equality (all fields/properties must match, and runtime types must match for record classes).
- Nondestructive mutation
- Records support
with expressions to create modified copies without changing the original instance.
-
ToString behavior
- Records synthesize a formatted
ToString that prints the type name and public property names/values.
- Classes inherit
object.ToString by default and usually need a custom override.
- Inheritance rules
- Record classes can inherit from other record classes, but:
- A record cannot inherit from a class.
- A class cannot inherit from a record.
- Record structs do not support inheritance.
Example of inheritance with record classes:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}
Equality in inheritance hierarchies for record classes is based on the runtime type, not just the base type. Two records are equal only if their runtime types and all their properties match.
Typical use cases
- API request/response models
- Configuration objects
- Value objects in domain-driven design (where identity is defined by values, not by an ID)
- Immutable snapshots of state
- Data returned from queries where equality should be based on the data, not the instance
Summary
- Use a class when modeling entities with identity, mutable state, or complex behavior.
- Use a record when modeling data-centric types where value equality, immutability, and concise syntax are desired.
References: