dotnet/C# Language
Records & value semantics
basicsTL;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 propertiesAmountandCurrency; - value equality:
Equals(object?),Equals(Money?),GetHashCode(),==and!=, each field compared throughEqualityComparer<T>.Default; - a
ToString()that printsMoney { 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 awithexpression 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. withis 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
EqualityContractproperty; two records with identical data but different declared types are never equal. Cross-hierarchy comparisons returnfalseby design. - Positional properties are
init, notreadonlyfields — reflection and serializers can still write them; records are immutable by convention, not by memory layout.
Further reading
- Records — C# language reference
- Value objects in DDD — records fit
- sharplab.io — see the lowered code yourself