~/handys11
Browse the docs

dotnet/C# Language

Records & value semantics

basics

TL;DR

// Positional record: immutable, value-equal, deconstructible — one line.
public record Money(decimal Amount, string Currency);

// Non-destructive mutation: copy everything, change one thing.
var price = new Money(9.99m, "EUR");
var discounted = price with { Amount = 7.99m };

// Value equality: contents, not references.
Console.WriteLine(price == new Money(9.99m, "EUR")); // True

// Deconstruction comes for free on positional records.
var (amount, currency) = discounted;

// Stack-allocated flavor for small data.
public readonly record struct Point(int X, int Y);

How it works

A record is not a new runtime concept — it is compiler-generated boilerplate over an ordinary class (or struct). For Money above, Roslyn synthesizes:

  • a primary constructor and init-only properties Amount and Currency;
  • value equality: Equals(object?), Equals(Money?), GetHashCode(), == and !=, each field compared through EqualityComparer<T>.Default;
  • a ToString() that prints Money { Amount = 9.99, Currency = EUR };
  • a Deconstruct(out decimal, out string) for positional syntax;
  • a copy constructor plus a hidden <Clone>$ method — that pair is all a with expression compiles down to.

Paste one into sharplab.io and read the lowered C#: it is exactly the class you would have written by hand, which is the point.

record class vs readonly record struct vs class

You want Reach for
Immutable domain value, heap is fine record (class)
Small value (≤ ~16 bytes), hot path readonly record struct
Identity that outlives its field values plain class

A record struct without readonly is mutable — usually not what you meant when you chose a record for value semantics.

Gotchas

  • Collections break value equality. A List<int> member is compared by reference, so two records with equal lists are not equal. Value equality is shallow — it stops at the first reference that isn’t itself value-equal.
  • with is a shallow copy. Mutable reference-type members are shared between the original and the copy.
  • Don’t use records for EF Core entities. Entities have identity semantics (same row, changing values); records have value semantics. Value equality also confuses change tracking and DbSet.Contains.
  • Inheritance plays by its own rules. Every record carries a hidden EqualityContract property; two records with identical data but different declared types are never equal. Cross-hierarchy comparisons return false by design.
  • Positional properties are init, not readonly fields — reflection and serializers can still write them; records are immutable by convention, not by memory layout.

Further reading