Records
Model immutable data with records — value equality, readable ToString and non-destructive changes.
10 min read
You will learn
- Declare a positional record
- Explain value equality vs reference equality
- Create modified copies with with-expressions
- Know when to choose a record over a class
Data-centric types
Many types in a program are mostly data: an address, a money amount, a point on a map, the body of an API response. For these, you usually want:
- values that don't change after creation (immutability),
- two objects with the same values to be equal,
- a readable
ToString()for logging and debugging.
Writing all that by hand in a class is tedious. A record gives it to you in one line.
Positional records
record Person(string Name, int Age);That single line generates:
- a constructor
Person(string Name, int Age), - init-only properties
NameandAge, - value-based
Equals,==,!=andGetHashCode, - a
ToString()that prints the values, - a
Deconstructmethod.
var ada = new Person("Ada", 36);
Console.WriteLine(ada); // Person { Name = Ada, Age = 36 }
Console.WriteLine(ada.Name); // Ada
ada.Age = 37; // ❌ compile error: init-only
var (name, age) = ada; // deconstructionValue equality
This is the biggest difference from classes:
class PersonClass { public string Name = ""; }
record PersonRecord(string Name);
var c1 = new PersonClass { Name = "Ada" };
var c2 = new PersonClass { Name = "Ada" };
Console.WriteLine(c1 == c2); // False — two different objects
var r1 = new PersonRecord("Ada");
var r2 = new PersonRecord("Ada");
Console.WriteLine(r1 == r2); // True — same valuesClasses use reference equality (same object?), records use value equality (same data?). Value equality makes records work naturally as dictionary keys, in sets, and in tests (Assert.Equal(expected, actual)).
with: non-destructive mutation
Since records are immutable, you don't change them — you create a modified copy:
var ada = new Person("Ada", 36);
var olderAda = ada with { Age = 37 };
Console.WriteLine(ada); // Person { Name = Ada, Age = 36 } — unchanged
Console.WriteLine(olderAda); // Person { Name = Ada, Age = 37 }Adding members
Records are still classes underneath — you can add methods, computed properties and validation:
record Money(decimal Amount, string Currency)
{
public Money Add(Money other)
{
if (other.Currency != Currency)
throw new InvalidOperationException("Currencies differ");
return this with { Amount = Amount + other.Amount };
}
public override string ToString() => $"{Amount:F2} {Currency}";
}
var total = new Money(10, "EUR").Add(new Money(5.5m, "EUR"));
Console.WriteLine(total); // 15.50 EURRecords can also use regular property syntax instead of the positional form, and can inherit from other records.
Record or class?
| Use a record for… | Use a class for… |
|---|---|
| Immutable data identified by its values | Objects with identity that change over time |
| DTOs, API requests/responses, messages, events | Services, repositories, controllers |
Value objects like Money, Address, DateRange | Entities like a Customer whose details evolve |
Note
Two customers with the same name are still two different customers — that's identity, so Customer is a class. Two Money(10, "EUR") values are interchangeable — that's a value, so Money is a record.
Try it
C# runtime not loaded
Notice how Items prints as System.String[] — the generated ToString calls each property's own ToString, and arrays don't print their contents.
Key takeaways
record Name(Type Prop, ...)generates constructor, init-only properties,ToString, deconstruction and value equality.- Two records with the same values are equal; two class instances usually aren't.
- Use
withto create modified copies of immutable data. - Records for values and data; classes for objects with identity and behavior.
Exercise
Point is a record. Implement Move, which returns a NEW point shifted by dx and dy (use a with-expression), and DistanceTo, which returns the straight-line distance to another point.
C# runtime not loaded
Hint 1
with creates a copy with some properties changed: this with { X = X + dx, Y = Y + dy }.
Hint 2
Distance is √(dx² + dy²): Math.Sqrt(Math.Pow(other.X - X, 2) + Math.Pow(other.Y - Y, 2)).
Show solution
var a = new Point(0, 0);
var b = a.Move(3, 4);
Console.WriteLine($"{a} → {b}, distance {a.DistanceTo(b)}");
record Point(int X, int Y)
{
public Point Move(int dx, int dy) => this with { X = X + dx, Y = Y + dy };
public double DistanceTo(Point other) =>
Math.Sqrt(Math.Pow(other.X - X, 2) + Math.Pow(other.Y - Y, 2));
}Quiz
4 questions · 3 correct to pass1.For a class `Person` (not a record), what is `new Person("Ada") == new Person("Ada")`?
2.What does `var older = ada with { Age = 37 };` do when ada is a record?
3.What does a positional record like `record Point(int X, int Y);` generate?
4.Which is the best fit for a record?
To complete this lesson
- ○ Solve the exercise
- ○ Pass the quiz (3/4 correct)