.NET Academy

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 Name and Age,
  • value-based Equals, ==, != and GetHashCode,
  • a ToString() that prints the values,
  • a Deconstruct method.
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;               // deconstruction

Value 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 values

Classes 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 EUR

Records 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 valuesObjects with identity that change over time
DTOs, API requests/responses, messages, eventsServices, repositories, controllers
Value objects like Money, Address, DateRangeEntities 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

Program.cs
Loading editor…

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 with to 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.

Program.cs
Loading editor…

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 pass
  1. 1.For a class `Person` (not a record), what is `new Person("Ada") == new Person("Ada")`?

  2. 2.What does `var older = ada with { Age = 37 };` do when ada is a record?

  3. 3.What does a positional record like `record Point(int X, int Y);` generate?

  4. 4.Which is the best fit for a record?

To complete this lesson

  • ○ Solve the exercise
  • ○ Pass the quiz (3/4 correct)