record, record struct, init, with expressions, value equality
Record Types in C#
Records were introduced in C# 9 and enhanced in C# 10.
They look like classes. They behave differently.
Most developers use records without fully understanding what makes them special — and more importantly, when they're the right tool and when they're not.
This guide breaks down everything you need to know.
What is a Record?
A record is a reference type (like a class) that is designed for immutable data with value-based equality.
public record Person(string Name, int Age);
That one line gives you:
- A constructor
- Public
init-only properties -
ToString()override -
Equals()andGetHashCode()based on values — not references - Deconstruction support
-
withexpression support
Value Equality — The Core Difference
With a class, two objects are equal only if they are the same object in memory.
With a record, two objects are equal if their values are the same.
Class (reference equality)
var p1 = new PersonClass { Name = "Alice", Age = 30 };
var p2 = new PersonClass { Name = "Alice", Age = 30 };
Console.WriteLine(p1 == p2); // False — different objects in memory
Record (value equality)
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
Console.WriteLine(p1 == p2); // True — same values
This is the single most important thing to understand about records.
Immutability With init
Record properties use init accessors by default — they can only be set during construction.
var person = new Person("Alice", 30);
person.Name = "Bob"; // ❌ Compile error — init-only
This makes records safe to pass around without defensive copying.
The with Expression
You cannot mutate a record. But you can create a modified copy using with.
var original = new Person("Alice", 30);
var updated = original with { Age = 31 };
Console.WriteLine(original); // Person { Name = Alice, Age = 30 }
Console.WriteLine(updated); // Person { Name = Alice, Age = 31 }
The original is untouched.
with creates a shallow copy with the specified properties replaced.
Deconstruction
Records support positional deconstruction automatically.
public record Person(string Name, int Age);
var person = new Person("Alice", 30);
var (name, age) = person;
Console.WriteLine(name); // Alice
Console.WriteLine(age); // 30
Record vs Class — Feature Comparison
| Feature | Record | Class |
|---|---|---|
| Type | Reference type | Reference type |
| Equality | Value-based | Reference-based |
| Immutability |
init by default |
Mutable by default |
with expression |
Yes | No |
| Inheritance | Supported | Supported |
ToString() |
Auto-generated | Default object.ToString() |
| Best for | DTOs, value objects, API responses | Entities, services, stateful objects |
Record Struct (C# 10)
C# 10 introduced record struct — a value type record.
public record struct Point(double X, double Y);
var a = new Point(1.0, 2.0);
var b = new Point(1.0, 2.0);
Console.WriteLine(a == b); // True — value equality
record struct gives you:
- Value type semantics (stack allocation, no GC pressure)
- Value equality
-
withexpression - No heap allocation
Use record struct for small, frequently used, immutable value objects.
When to Use Records
Use records for
- DTOs (Data Transfer Objects) — data flowing between layers
- API request/response models
- Value objects in Domain-Driven Design
- Query results that should not be mutated
- Configuration objects
- Any data where equality means "same values"
Do NOT use records for
-
Domain entities that have identity (an
Orderwith an ID is not equal to anotherOrderwith the same values — they are separate things) - Services and repositories — these have behaviour, not just data
- Objects with complex mutable state
Real-World Example
// ✅ Perfect use of record — API response DTO
public record OrderResponse(
Guid Id,
string CustomerName,
decimal TotalAmount,
DateTime CreatedAt
);
// ✅ With expression for building variations in tests
var baseOrder = new OrderResponse(Guid.NewGuid(), "Alice", 150.00m, DateTime.UtcNow);
var updatedOrder = baseOrder with { TotalAmount = 200.00m };
// ✅ Value equality in assertions
var expected = new OrderResponse(id, "Alice", 150.00m, createdAt);
var actual = service.GetOrder(id);
Assert.Equal(expected, actual); // Works because of value equality
Record Inheritance
Records support inheritance — but with one rule:
A record can only inherit from another record (not a class).
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 }
Equality checks include all derived type properties.
Interview-Ready Summary
- Records are reference types with value-based equality
- Two records with the same values are considered equal
- Properties are
init-only by default — immutable after construction - Use
withto create modified copies without mutating the original -
record structis a value type record — no heap allocation - Use records for DTOs, API models, value objects
- Do not use records for domain entities that have identity
A strong interview answer:
"Records are designed for immutable data with value-based equality. Unlike classes where equality means same reference, two records with the same values are equal. The with expression lets you produce modified copies non-destructively. They're ideal for DTOs and value objects, but not for domain entities where identity matters more than content."
⭐ Add-On — Records in Pattern Matching
Records work exceptionally well with C# pattern matching.
public record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
double GetArea(Shape shape) => shape switch
{
Circle(var r) => Math.PI * r * r,
Rectangle(var w, var h) => w * h,
_ => throw new ArgumentException("Unknown shape")
};
Positional patterns work directly with record deconstruction — no extra code needed.
Top comments (0)