TL;DR: Use Records for immutable data transfer objects (DTOs), configuration settings, and value-based equality. Use Classes for business logic, services, and mutable entities (like EF Core models).
When Microsoft introduced record types in C# 9, it fundamentally changed how we model data in .NET. But with so many options—class, struct, record class, record struct—how do you know which one to pick?
In this guide, we'll demystify Records and explain exactly when you should (and shouldn't) use them.
The Problem with Classes
By default, standard C# classes use Reference Equality. This means that two instances of a class are only considered "equal" if they point to the exact same location in memory.
❌ The Class Equality Problem
public class UserDto
{
public string Name { get; set; }
}
var user1 = new UserDto { Name = "Ajay" };
var user2 = new UserDto { Name = "Ajay" };
Console.WriteLine(user1 == user2); // FALSE!
To fix this, you would historically have to write dozens of lines of boilerplate code to override .Equals(), .GetHashCode(), and implement IEquatable<T>.
Enter the Record
A record is just a class (or a struct) under the hood, but the C# compiler automatically generates all that boilerplate for you. Records use Value Equality.
✅ The Record Solution
public record UserDto(string Name);
var user1 = new UserDto("Ajay");
var user2 = new UserDto("Ajay");
Console.WriteLine(user1 == user2); // TRUE!
Notice how much cleaner that is? A single line of code replaces an entire file of boilerplate.
Non-Destructive Mutation (The with Expression)
Because Records are typically immutable, you can't just change a property. Instead, you create a copy of the record with modified properties using the with keyword.
public record Product(string Name, decimal Price);
var p1 = new Product("Laptop", 1000m);
// Create a copy of p1, but with a different Price
var p2 = p1 with { Price = 800m };
Console.WriteLine(p1.Price); // 1000 (Original is unchanged)
Console.WriteLine(p2.Price); // 800
When to Use What?
Use Records For:
- API DTOs: Incoming requests and outgoing responses.
- Configuration: Strongly-typed configuration objects mapped from
appsettings.json. - Event Sourcing: Messages and events that have happened in the past and cannot be altered.
Use Classes For:
- Entity Framework Models: EF Core relies heavily on reference tracking and mutability.
- Services: Your DI injected services (
OrderService,EmailSender) should be classes. - UI State: Components in Blazor, WPF, or MAUI that require complex state mutation.
Summary
Records are the ultimate tool for modeling data in modern C#. Whenever you find yourself writing a class just to hold data with no complex behavior, make it a record instead!