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.

Leave a Comment

Your email address will not be published. Required fields are marked *