C# Records
A record is a special type introduced in C# 9 designed for storing data. Records are immutable by default, support value-based equality, and generate useful methods automatically — saving you from writing repetitive boilerplate code.
Why Records Exist
Before records, creating a simple data-holding class required a lot of manual code: a constructor, equality comparisons, a ToString() override, and so on. Records generate all of this for you automatically.
┌──────────────────────────────────────────────────────────────┐
│ WITHOUT RECORDS — Manual work: │
├──────────────────────────────────────────────────────────────┤
│ class Person │
│ { │
│ public string Name { get; init; } │
│ public int Age { get; init; } │
│ public Person(string name, int age) │
│ { Name = name; Age = age; } │
│ public override string ToString() │
│ { return $"Person {{ Name={Name}, Age={Age} }}"; } │
│ public override bool Equals(object obj) { ... } │
│ public override int GetHashCode() { ... } │
│ } │
├──────────────────────────────────────────────────────────────┤
│ WITH RECORDS — One line: │
├──────────────────────────────────────────────────────────────┤
│ record Person(string Name, int Age); │
│ // Constructor, Equals, GetHashCode, ToString — all done! │
└──────────────────────────────────────────────────────────────┘
Declaring a Record
Positional Syntax (shortest form)
record Person(string Name, int Age);
// Use it:
Person p = new Person("Alice", 30);
Console.WriteLine(p.Name); // Alice
Console.WriteLine(p.Age); // 30
Console.WriteLine(p); // Person { Name = Alice, Age = 30 }
Full Property Syntax
record Product
{
public string Name { get; init; }
public double Price { get; init; }
public string Category { get; init; }
}
Product laptop = new Product
{
Name = "Laptop",
Price = 999.99,
Category = "Electronics"
};
init — Set Once, Then Immutable
The init accessor means a property can only be set during object creation. After that, it is read-only.
record Car(string Model, int Year);
Car c = new Car("Tesla", 2024);
// c.Model = "BMW"; ❌ ERROR — cannot change after creation
Console.WriteLine(c.Model); // Tesla
Value-Based Equality
Classes compare by reference — two objects are equal only if they point to the same memory. Records compare by value — two records are equal if all their properties match, even if they are separate objects.
record Point(int X, int Y); Point a = new Point(3, 5); Point b = new Point(3, 5); Point c = new Point(1, 2); Console.WriteLine(a == b); // True ← same values Console.WriteLine(a == c); // False ← different values Console.WriteLine(a.Equals(b)); // True
Equality Diagram
┌──────────────────────────────────────────────────────────────┐
│ CLASS equality (reference): │
│ Car a = new Car("X5"); │
│ Car b = new Car("X5"); │
│ a == b → FALSE (different objects in memory) │
│ │
│ RECORD equality (value): │
│ record Car(string Model); │
│ Car a = new Car("X5"); │
│ Car b = new Car("X5"); │
│ a == b → TRUE (same property values) │
└──────────────────────────────────────────────────────────────┘
ToString() — Auto-Generated
Records automatically produce a readable string showing all property names and values.
record Student(string Name, int Grade, double GPA);
Student s = new Student("Bob", 10, 3.8);
Console.WriteLine(s);
// Output: Student { Name = Bob, Grade = 10, GPA = 3.8 }
with — Non-Destructive Mutation
Since records are immutable, you cannot change them. But you can create a copy with some properties changed using the with keyword. The original stays untouched.
record Address(string Street, string City, string Country);
Address home = new Address("Baker St", "London", "UK");
// Create a new address with only City changed:
Address office = home with { City = "Manchester" };
Console.WriteLine(home); // Address { Street = Baker St, City = London, Country = UK }
Console.WriteLine(office); // Address { Street = Baker St, City = Manchester, Country = UK }
with Expression Diagram
┌────────────────────────────────────────────────────────────────────────┐
│original record: { Street="Baker St", City="London", Country="UK" } |
│ │ │
│ ▼ with { City = "Manchester" } │
│new record: { Street="Baker St", City="Manchester", Country="UK" } |
│ │
│ Original is unchanged — a new record is returned │
└────────────────────────────────────────────────────────────────────────┘
Records Support Methods and Constructors
record Circle(double Radius)
{
// Computed property
public double Area => Math.PI * Radius * Radius;
// Method
public void Describe()
{
Console.WriteLine($"Circle with radius {Radius}, area {Area:F2}");
}
// Custom validation in constructor
public Circle : this(Radius) // call generated constructor
{
if (Radius <= 0)
throw new ArgumentException("Radius must be positive.");
}
}
Circle c = new Circle(5);
c.Describe(); // Circle with radius 5, area 78.54
Record Inheritance
Records can inherit from other records. A derived record adds new properties while keeping those of the base record.
record Animal(string Name, string Species);
record Pet(string Name, string Species, string OwnerName)
: Animal(Name, Species);
Pet dog = new Pet("Rex", "Dog", "Alice");
Console.WriteLine(dog);
// Pet { Name = Rex, Species = Dog, OwnerName = Alice }
Record Struct (C# 10+)
C# 10 added record struct — a value-type record that combines struct efficiency with record convenience.
record struct Coordinate(double Lat, double Lng);
Coordinate loc = new Coordinate(28.6, 77.2);
Console.WriteLine(loc); // Coordinate { Lat = 28.6, Lng = 77.2 }
Records vs Classes vs Structs
┌───────────────────┬───────────────┬───────────────┬────────────────┐ │ Feature │ Class │ Struct │ Record │ ├───────────────────┼───────────────┼───────────────┼────────────────┤ │ Type │ Reference │ Value │ Reference* │ │ Equality │ Reference │ Value │ Value │ │ Mutable │ Yes │ Yes │ No (init) │ │ Inheritance │ Yes │ No │ Yes (record) │ │ ToString │ Manual │ Manual │ Auto │ │ with expression │ No │ No │ Yes │ │ Best for │ Complex OOP │ Small data │ Immutable data │ └───────────────────┴───────────────┴───────────────┴────────────────┘ * record struct is a value type
When to Use Records
Use records for: ✅ Data transfer objects (DTOs) ✅ API response models ✅ Configuration objects ✅ Immutable data snapshots ✅ Any scenario where data = identity (two objects with same data = same thing) Avoid records when: ❌ Object needs frequent mutations ❌ Object identity matters (two orders with same data are still different orders) ❌ Complex behaviour and inheritance hierarchies
Quick Summary
┌──────────────────────────────────────────────────────────────┐ │ record Person(string Name, int Age); ← one-line definition │ │ │ │ Auto-generated: constructor, Equals, GetHashCode, ToString │ │ Immutable: properties use init — set once only │ │ Value equality: two records equal if all values match │ │ with expression: copy with selective changes │ │ Inheritance: record can extend another record │ └──────────────────────────────────────────────────────────────┘
Records eliminate the tedious boilerplate of data classes. They are the cleanest way to represent data in modern C# — especially in APIs, domain models, and any scenario where you want safe, predictable, immutable data flowing through your program.
