What a record is (and the two flavors)
record(record class) is a reference type with value-based equality.record structis a value type with value-based equality (and no allocation for locals).- Positional records (
record Person(string Name, int Age)) synthesize properties, equality,Deconstruct, andToString. - Nominal records (
record Person { public string Name { get; init; } }) give you the same semantics but you declare the properties yourself.
Benefits of records
- Value equality by default: two records with the same data are equal.
- Nondestructive mutation:
withcreates a copy with a few changed members. - Concise models: less boilerplate for DTOs, value objects, and messages.
- Good debugging output:
ToStringprints property names and values. - Immutability is encouraged:
init-only setters work well with records.
Drawbacks of records
- Equality can be surprising: records compare by data, not by reference identity.
- Shallow copying:
withcopies the top-level members; nested reference types are shared. - Versioning risks: adding a property changes equality and
ToString, which can be a breaking change. - Potential for accidental mutability: if you use
setinstead ofinit, you can break invariants and hash collections.
Gotchas to watch for
Gotcha 1: with is shallow
public record Person(string Name, List<string> Tags);
var a = new Person("Ada", new List<string> { "admin" });
var b = a with { Name = "Grace" };
b.Tags.Add("ops");
// a.Tags now also contains "ops"
Use immutable collections (like ImmutableArray<T> or ImmutableList<T>)
if you want a deep immutability story.
Gotcha 2: equality includes all instance data
Adding a new property changes Equals, GetHashCode, and ToString.
public record Order(string Id)
{
public string? CorrelationId { get; init; } // now part of equality
}
That is great when the new member is part of identity, and harmful when it is not. Be deliberate.
Gotcha 3: ToString can leak data
Records print all value members by default. If a record can contain secrets,
override ToString or PrintMembers.
Gotcha 4: inheritance has strict equality rules
Two record instances are equal only if they are the same runtime type and
all value members are equal. A User record will not equal an AdminUser
record even if the data matches.
Gotcha 5: mutation breaks hashing
If a record is used as a dictionary key or hash set element, any mutation of value members after insertion can make it "disappear" from the collection.
Mutable records: impact and guidance
What changes when records are mutable?
withbecomes less valuable (you can just mutate).- Equality and hashing become unstable if value members can change.
- Reasoning becomes harder because a record looks immutable to many readers.
When mutable records are appropriate
- Internal types with very short lifetimes (tests, internal DTOs).
- Types that never appear as dictionary keys or hash set elements.
- Places where you explicitly want value equality but not immutability.
When mutable records are inappropriate
- Public API types (callers will assume immutability).
- Types used as keys in hash-based collections.
- Types with strong invariants that must always hold.
Primary constructor parameter vs. property
The difference
- The primary constructor parameter is an input value.
- The property is the stored value that participates in equality,
Deconstruct, andToString.
In a positional record, the compiler turns each primary constructor parameter into a public property and wires it into all the generated members.
public record Person(string Name);
// Equivalent to:
// public string Name { get; init; }
If you need custom behavior, you can explicitly declare the property and the compiler will use your version.
Customizing a property with a primary constructor
1) Normalize or validate in the init accessor
public record Person(string Name)
{
private string _name = string.Empty;
public string Name
{
get => _name;
init => _name = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Name required", nameof(Name))
: value.Trim();
}
}
This keeps the positional syntax while enforcing invariants.
2) Add derived properties while keeping the positional ones
public record EmailAddress(string Value)
{
public string Domain => Value.Split('@')[1];
}
Derived properties are also part of equality, so only add them if that is what you want.
3) Switch to nominal form when the property logic dominates
public record Money
{
public decimal Amount { get; init; }
public string Currency { get; init; } = "USD";
}
Nominal records are clearer when the primary constructor is no longer the center of the design.
When to choose records (rules of thumb)
- You want value semantics for a data-centric type.
- Immutability is the default (use
init). - You want concise models for DTOs, events, and value objects.
- You can accept that equality and hashing are based on member values.
If you need identity, complex lifecycle, or polymorphic behavior with mutable state, prefer a class instead.
Final thoughts
- Records are great for small, immutable data shapes with clear value semantics.
withis a convenience, not a deep clone.- If you choose mutability, do it knowingly and keep records away from hash-based collections.


