C# Class vs Record: Differences, Use Cases, and Real-World Project Examples
In this article, we will explore the differences between class and record in C# with practical software development examples.
1. What is a Class?
A class is the fundamental reference type in C# used for creating objects.
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
var user1 = new User { Id = 1, Name = "Emre" };
var user2 = user1;
user2.Name = "Ahmet";
Console.WriteLine(user1.Name); // Ahmet
Both variables reference the same object in memory.
---
## 2. What is a Record?
`record` was introduced in C# 9 and is designed primarily for data-centric models.
```csharp
public record UserDto(int Id, string Name);
var user1 = new UserDto(1, "Emre");
var user2 = new UserDto(1, "Emre");
Console.WriteLine(user1 == user2); // True
Records provide value-based equality by default.
3. The Core Difference Between Class and Record
For a class:
Is this the same object?
For a record:
Do these objects contain the same data?
Example:
public class ProductClass
{
public int Id { get; set; }
public string Name { get; set; }
}
public record ProductRecord(int Id, string Name);
var c1 = new ProductClass { Id = 1, Name = "Laptop" };
var c2 = new ProductClass { Id = 1, Name = "Laptop" };
var r1 = new ProductRecord(1, "Laptop");
var r2 = new ProductRecord(1, "Laptop");
Console.WriteLine(c1 == c2); // False
Console.WriteLine(r1 == r2); // True
4. Why Classes Are Better for Mutable Objects
Classes are typically used for objects whose state changes over time.
public class Basket
{
public int Id { get; set; }
public List<string> Products { get; set; } = new();
public void AddProduct(string product)
{
Products.Add(product);
}
}
5. Why Records Are Better for Immutable Data
Records are ideal for DTOs, requests, responses, and value objects.
public record CreateUserRequest(string Name, string Email);
6. Using the with Expression
One of the most powerful features of records is the with expression.
public record UserDto(int Id, string Name, string Email);
var user1 = new UserDto(1, "Emre", "[email protected]");
var user2 = user1 with
{
Email = "[email protected]"
};
This allows immutable-style object updates.
7. ToString Differences
Class:
Console.WriteLine(user);
// Namespace.UserClass
Record:
Console.WriteLine(user);
// UserRecord { Id = 1, Name = Emre }
Records provide cleaner debug output by default.
8. Built-in Deconstruction Support
Records automatically support deconstruction.
public record UserDto(int Id, string Name);
var user = new UserDto(1, "Emre");
var (id, name) = user;
9. Why Entities Usually Use Classes
Entities are identity-based and behavior-oriented.
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public void ChangePrice(decimal newPrice)
{
if (newPrice <= 0)
throw new ArgumentException();
Price = newPrice;
}
}
Entities contain business logic and mutable state.
10. Why DTOs Are Great Candidates for Records
public record ProductResponse(
int Id,
string Name,
decimal Price
);
DTOs mainly carry data and rarely contain behavior.
11. Records for Value Objects
public record Money(decimal Amount, string Currency);
var money1 = new Money(100, "TRY");
var money2 = new Money(100, "TRY");
Console.WriteLine(money1 == money2); // True
Value objects are naturally value-based.
12. Why Services and Repositories Should Be Classes
public class PaymentService
{
public void Pay(decimal amount)
{
// payment logic
}
}
Services contain behavior, not just data.
13. Records in CQRS and MediatR
public record CreateProductCommand(
string Name,
decimal Price
);
public record GetProductByIdQuery(int Id);
14. record class vs record struct
public record Person(string Name);
public record struct Point(int X, int Y);
public readonly record struct Coordinate(int X, int Y);
record class→ reference typerecord struct→ value type
15. Real-World Project Example
Entity:
public class Order
{
public int Id { get; private set; }
public int CustomerId { get; private set; }
public decimal TotalAmount { get; private set; }
public string Status { get; private set; }
public void Approve()
{
if (Status != "Pending")
throw new InvalidOperationException();
Status = "Approved";
}
}
DTO:
public record OrderResponse(
int Id,
int CustomerId,
decimal TotalAmount,
string Status
);
Command:
public record CreateOrderCommand(
int CustomerId,
List<int> ProductIds
);
Value Object:
public record OrderPrice(decimal Amount, string Currency);
16. Quick Decision Table
| Scenario | Preferred Type |
|---|---|
| Entity | class |
| Service | class |
| Repository | class |
| Mutable Object | class |
| DTO | record |
| API Request | record |
| API Response | record |
| Value Object | record |
| Event Model | record |
| Command/Query | record |
17. Final Rule of Thumb
Use class when:
- Identity matters
- The object contains behavior
- The object changes over time
Use record when:
- The object only carries data
- You prefer immutable design
- Value-based equality is needed
Practical mindset:
Does this object perform actions? → class
Does this object only carry data? → record
Does identity matter? → class
Do values matter more? → record
In professional .NET projects, the most common approach is:
classfor entities, services, repositories, and behavior-heavy modelsrecordfor DTOs, requests, responses, commands, queries, and value objects
18. Quick Decision Table
| Scenario | Preferred Type |
|---|---|
| Entity | class |
| Service | class |
| Repository | class |
| Controller | class |
| Manager | class |
| Mutable Object | class |
| DTO | record |
| API Request/Response | record |
| Value Object | record |
| Event Model | record |
| Command/Query Model | record |
| Config/Options Model | class or record |
| EF Core Entity | usually class |
| Log/Event Payload | record |
19. Real-World Project Example
Entity:
public class Order
{
public int Id { get; private set; }
public int CustomerId { get; private set; }
public decimal TotalAmount { get; private set; }
public string Status { get; private set; }
public void Approve()
{
if (Status != "Pending")
throw new InvalidOperationException("Only pending orders can be approved.");
Status = "Approved";
}
}
DTO:
public record OrderResponse(
int Id,
int CustomerId,
decimal TotalAmount,
string Status
);
Command:
public record CreateOrderCommand(
int CustomerId,
List<int> ProductIds
);
Value Object:
public record OrderPrice(decimal Amount, string Currency);
The distinction here is clear:
Orderis an entity →classOrderResponsecarries data →recordCreateOrderCommandcarries data →recordOrderPriceis a value object →record
20. The Most Practical Rule
Use class when:
- Identity matters
- The object contains behavior
- The object changes over time
Use record when:
- The object only carries data
- You prefer immutable design
- You need value-based equality
A practical mindset:
Does this object perform actions? → class
Does this object only carry data? → record
Does identity matter? → class
Do values matter more? → record
Does the object change over time? → class
Should the object remain immutable after creation? → record
In professional .NET projects, the most common approach is:
- Use
classfor entities, services, repositories, and behavior-heavy models - Use
recordfor DTOs, requests, responses, commands, queries, and value objects