Modifying objects causes bugs when multiple parts of code reference the same instance. Records with with-expressions create modified copies safely.
The Mutable Class Problem:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
var person = new Person { Name = "John", Age = 30 };
var olderPerson = person; // Same reference!
olderPerson.Age = 31;
Console.WriteLine(person.Age); // 31 (UNEXPECTED!)
// Modifying olderPerson modified original person
// They're the same object!
The Record Solution:
public record Person(string Name, int Age);
var person = new Person("John", 30);
var olderPerson = person with { Age = 31 }; // Creates NEW instance
Console.WriteLine(person.Age); // 30 (unchanged)
Console.WriteLine(olderPerson.Age); // 31
// Two separate objects, original is immutable
How With-Expressions Work:
// With-expression creates a copy with modifications
var original = new Person("John", 30);
// Modify one property
var modified = original with { Age = 31 };
// Modify multiple properties
var differentPerson = original with
{
Name = "Jane",
Age = 25
};
// Original unchanged
Console.WriteLine(original); // Person { Name = John, Age = 30 }
Console.WriteLine(modified); // Person { Name = John, Age = 31 }
Console.WriteLine(differentPerson); // Person { Name = Jane, Age = 25 }
Nested Records:
public record Address(string Street, string City);
public record Person(string Name, Address Address);
var person = new Person(
"John",
new Address("123 Main St", "Boston")
);
// Modify nested record
var movedPerson = person with
{
Address = person.Address with { City = "New York" }
};
Console.WriteLine(person.Address.City); // Boston (unchanged)
Console.WriteLine(movedPerson.Address.City); // New York
Use in Pipelines:
public record Product(string Name, decimal Price, bool IsActive); var products = new List{ new("Laptop", 1000m, true), new("Mouse", 25m, false), new("Keyboard", 75m, true) }; // Apply 10% discount to active products var discountedProducts = products .Where(p => p.IsActive) .Select(p => p with { Price = p.Price * 0.9m }) .ToList(); // Original list unchanged Console.WriteLine(products[0].Price); // 1000 Console.WriteLine(discountedProducts[0].Price); // 900
State Management Pattern:
public record AppState(
string CurrentUser,
bool IsLoading,
List Errors
);
public class StateManager
{
private AppState _state = new AppState("", false, new List());
public AppState GetState() => _state;
public void SetLoading(bool isLoading)
{
_state = _state with { IsLoading = isLoading };
}
public void SetUser(string username)
{
_state = _state with { CurrentUser = username };
}
public void AddError(string error)
{
var newErrors = new List(_state.Errors) { error };
_state = _state with { Errors = newErrors };
}
}
// Immutable state updates
// Easy to track changes
// Can implement undo/redo
Equality and Comparison:
var person1 = new Person("John", 30);
var person2 = new Person("John", 30);
var person3 = person1 with { }; // Exact copy
// Value-based equality (not reference equality)
Console.WriteLine(person1 == person2); // true (same values)
Console.WriteLine(person1 == person3); // true
// Classes would be false:
// public class PersonClass { ... }
// var p1 = new PersonClass { Name = "John" };
// var p2 = new PersonClass { Name = "John" };
// p1 == p2; // false (different objects)
Deconstruction:
var person = new Person("John", 30);
// Deconstruct into variables
var (name, age) = person;
Console.WriteLine(name); // John
Console.WriteLine(age); // 30
// Useful in pattern matching
var greeting = person switch
{
("John", var age) when age > 25 => "Hi John, you're over 25",
(var name, 30) => $"Hi {name}, you're exactly 30",
_ => "Hello"
};
Performance:
// Benchmark: Create 1 million modified copies
// Method 1: Manual copy with class
public class PersonClass
{
public string Name { get; set; }
public int Age { get; set; }
public PersonClass Copy() => new PersonClass
{
Name = this.Name,
Age = this.Age
};
}
// Time: 250ms
// Allocations: 95 MB
// Method 2: Record with with-expression
public record PersonRecord(string Name, int Age);
// Time: 180ms (28% faster!)
// Allocations: 85 MB (10% less memory)
// Compiler optimizes with-expressions
When to Use Records:
✅ Use Records for: - DTOs (Data Transfer Objects) - API request/response models - Configuration objects - Value objects (DDD) - State management - Immutable data pipelines ❌ Use Classes for: - Entities with behavior/methods - Objects that need to be mutated in-place - Large objects (copying is expensive) - Objects with complex lifecycle
