๐ฆ Value Objects Made Easy
DTOs with 50 lines of boilerplate? Equals(), GetHashCode(), ToString()? Record types (C# 9+) are immutable, value-based, with auto-generated methods. One line replaces 50.
โ Traditional Class (40+ lines)
public class Person
{
public string Name { get; init; }
public int Age { get; init; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
public override bool Equals(object? obj)
{
if (obj is not Person other) return false;
return Name == other.Name && Age == other.Age;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, Age);
}
public override string ToString()
{
return $"Person {{ Name = {Name}, Age = {Age} }}";
}
}
โ Record (1 line!)
public record Person(string Name, int Age); // That's it! Auto-generates: // - Constructor // - Value-based Equals/GetHashCode // - ToString() // - With expressions // - Deconstruct()
๐ฏ Record Features
var person1 = new Person("Alice", 30);
var person2 = new Person("Alice", 30);
Console.WriteLine(person1 == person2); // True! (value equality)
// With expressions (non-destructive mutation)
var person3 = person1 with { Age = 31 };
Console.WriteLine(person3); // Person { Name = Alice, Age = 31 }
// person1 unchanged (immutable)
// Deconstruction
var (name, age) = person1;
Console.WriteLine($"{name} is {age}"); // Alice is 30
// Pattern matching
var message = person1 switch
{
{ Age: < 18 } => "Minor",
{ Age: >= 18 and < 65 } => "Adult",
_ => "Senior"
};
๐ Perfect for DTOs
// API Request/Response DTOs
public record CreateUserRequest(
string Email,
string Name,
string Password
);
public record UserResponse(
int Id,
string Email,
string Name,
DateTime CreatedAt
);
// API Controller
[HttpPost("users")]
public async Task CreateUser(CreateUserRequest request)
{
var user = await _userService.CreateAsync(request);
return new UserResponse(
user.Id,
user.Email,
user.Name,
user.CreatedAt
);
}
// Clean, immutable, perfect for data transfer
๐ก Best Practices
- Use for DTOs: API requests/responses, messages
- Use for value objects: Money, Address, Coordinate
- Add validation: In constructor or properties
- Keep immutable: Use init, not set
- Prefer positional: More concise for simple records
“Converted all DTOs from classes to records. Deleted 2000+ lines of boilerplate. Bugs related to mutable state disappeared. API responses now immutable, thread-safe. Should’ve used records from day one.”
