Stop Using Records for Everything: C# Classes vs Records
When Microsoft introduced record types in C# 9, the community reacted exactly how you’d expect: we started replacing the word class with record everywhere. DTOs? Records. API Responses? Records. Entity Framework Models? Records!
It felt like the ultimate hack to write less code. You just declare public record User(string Name); and boom—you get properties, a constructor, a deconstructor, and value equality all for free.
But after dealing with the fallout of the "Record Everything" craze in large production codebases, I’m here to offer some pragmatic advice: Classes are not dead, and you are probably overusing records.
What Makes Records Special?
To understand when to use them, you have to understand what the compiler actually does when you type record. It generates a standard class (or struct), but injects two crucial things:
- Immutability by Default (with Positional Syntax): The properties generated by
record User(string Name)areinit-only. They cannot be changed after the object is created. - Value-based Equality: If you compare two different instances of a class with the same data,
obj1 == obj2isfalse(because they have different memory references). If you compare two records with the same data,rec1 == rec2istrue.
When You MUST Use Records
Records shine when data is the primary concern, and the data shouldn't change.
1. API Requests and Responses (DTOs)
If you are returning JSON from an API, or accepting a JSON payload, records are perfect. Data Transfer Objects should strictly be read-only carriers of data.
// Perfect use case. Clean, immutable, zero boilerplate.
public record CreateOrderRequest(Guid UserId, decimal Amount, string Currency);
2. MediatR Commands and Queries
If you use CQRS or MediatR, your commands and queries should be immutable. A query shouldn't change while it's being processed.
public record GetUserQuery(Guid UserId) : IRequest<UserDto>;
3. Configuration Objects (IOptions)
When binding appsettings.json to strong types using the Options pattern, records provide a safe, read-only representation of your config.
When You Should NEVER Use Records
This is where developers shoot themselves in the foot.
1. Entity Framework Core Entities
Do not use records for EF Core database entities. EF Core relies heavily on tracking changes to the properties of an object over time. Records are designed to be immutable.
If you make your EF models immutable, you force yourself to use the with expression to create entirely new objects just to update a user's name, which destroys EF Core's Change Tracker.
// 🚨 Anti-pattern: EF Core hates this
public record DbUser(Guid Id, string Name);
Stick to standard class types with mutable properties (and private setters for encapsulation) for Domain models and EF Entities.
2. Services and Business Logic
Never use a record for a service class (e.g., PaymentService). Services possess behavior, not data. Comparing two PaymentService instances for value equality makes zero logical sense.
// 🚨 Nonsense
public record PaymentService(IPaymentGateway Gateway);
Note: With C# 12 Primary Constructors, you can now write public class PaymentService(IPaymentGateway Gateway) to get the clean syntax without the unnecessary record baggage.
The "Value Equality" Performance Trap
Because records generate value-based equality, the compiler writes a hidden Equals method that compares every single property of the record.
If you have a massive record with 50 properties, and you put thousands of them in a HashSet or frequently compare them, that generated Equals method is going to burn CPU cycles looping through strings and deep comparisons. A standard class just compares memory references (a single CPU instruction).
The Verdict
Records are not a replacement for classes. They are a tool for modeling immutable, value-like data structures.
Use records for DTOs, Messages, and Commands. Use classes for Entities, Services, and anything that changes state.