Records vs classes in C#
Records give you value equality, with expressions and a one-line declaration. Here's when that helps, and the two places it quietly bites.
C# records have been around since C# 9, and they're still one of the most useful features for everyday backend code. They're also easy to reach for in the wrong place. Here's how I decide.
What a record gives you
public record OrderPlaced(Guid OrderId, string CustomerId, decimal Total);
That one line gives you:
- A constructor and
init-only properties for each parameter - Value equality: two instances with the same values are
Equalsand== - A readable
ToString():OrderPlaced { OrderId = ..., CustomerId = ..., Total = ... } - Non-destructive mutation with
with
var original = new OrderPlaced(id, "C-42", 100m);
var corrected = original with { Total = 95m }; // a new instance; original is unchanged
Where records shine
Messages and events. A message describes something that happened. It shouldn't change after it's created, and two messages with the same content really are the same message. Immutability and value equality fit exactly.
DTOs at the edges. Request and response models, API client payloads and query results are data with no behavior. A positional record is the shortest honest way to declare them.
Small value objects. Money, a date range or an email address. Value equality is the whole point of a value object, and records give it to you for free.
Where to use a class instead
Entities. An order is still the same order after its total changes. Its identity is its ID, not its contents, so value equality is the wrong model. EF Core also tracks entities by reference and expects to change them, which fights with init-only properties. Keep entities as classes.
Anything with a lot of behavior or mutable state. Services, handlers and builders aren't data. Making them records adds equality semantics nobody wants.
Two gotchas
Collections compare by reference. Value equality is only as deep as each property's own Equals:
public record Basket(string Id, List<string> Items);
var a = new Basket("b1", ["apple"]);
var b = new Basket("b1", ["apple"]);
Console.WriteLine(a == b); // False: the two lists are different objects
If equality matters, use an immutable collection with structural comparison, or override Equals yourself.
with makes a shallow copy. The new record shares the same list instance as the old one. Changing the list through one changes it for both. Another reason to keep record members immutable all the way down.
record struct
record struct gives you the same features on a value type. Note that a positional record struct has mutable properties by default. Write readonly record struct if you want the immutability you'd expect from a record class.
Takeaway
Use records for data that describes something: messages, DTOs and value objects. Use classes for things with identity and behavior, like entities and services. And keep record members immutable, because value equality and shared mutable lists don't mix.