Records (introduced in C# 9) are a special class (or struct) that are designed for immutable data with value-based equality. Two record instances are equal if all their properties are equal — unlike classes where two different objects are only equal if they are literally the same reference. Records also get a free ToString(), deconstruction, and the with expression for creating modified copies. They are the natural choice for DTOs, domain value objects, API request/response models, and domain events throughout an ASP.NET Core application.
Record Class
// Positional record — concise syntax, compiler generates everything
public record Point(double X, double Y);
// Equivalent to a class with:
// - public double X { get; init; }
// - public double Y { get; init; }
// - Constructor: public Point(double X, double Y)
// - Equality: based on X and Y values
// - ToString: "Point { X = 1, Y = 2 }"
// - Deconstruction: var (x, y) = point;
var p1 = new Point(1.0, 2.0);
var p2 = new Point(1.0, 2.0);
var p3 = new Point(3.0, 4.0);
Console.WriteLine(p1 == p2); // true — value equality (same X and Y)
Console.WriteLine(p1 == p3); // false — different values
Console.WriteLine(p1); // "Point { X = 1, Y = 2 }"
// Deconstruction
var (x, y) = p1;
Console.WriteLine($"x={x}, y={y}"); // x=1, y=2
Equals(). This makes records ideal for caching (dictionary keys), deduplication, and domain value objects where two objects with the same data should be considered the same thing (e.g., two Money(100, "USD") instances should be equal). For entity models in EF Core, you usually want reference equality — use classes, not records, for entities.with expression to create a modified copy of a record without mutating the original. This is called non-destructive mutation: var updated = original with { Name = "Bob" } creates a new record with all the same property values except Name. This pattern is heavily used in functional-style code and domain event sourcing. It is the idiomatic way to “update” an immutable record in application logic — update a DTO’s field before sending it, apply a discount to a price value object, etc.The With Expression — Non-Destructive Mutation
public record CreatePostRequest(
string Title,
string Body,
string Slug,
string[] Tags
);
var original = new CreatePostRequest(
Title: "Hello World",
Body: "This is my first post.",
Slug: "hello-world",
Tags: new[] { "general" }
);
// Create a modified copy — original is unchanged
var updated = original with { Tags = new[] { "general", "beginner" } };
Console.WriteLine(original.Tags.Length); // 1
Console.WriteLine(updated.Tags.Length); // 2
// With expression in domain logic — apply a discount
public record Money(decimal Amount, string Currency);
var price = new Money(100.00m, "USD");
var sale = price with { Amount = price.Amount * 0.90m }; // 10% discount
Console.WriteLine(sale); // "Money { Amount = 90.00, Currency = USD }"
Record Structs (C# 10)
// record struct — value type with value equality (no heap allocation)
// Best for small, frequently-used value objects
public record struct Coordinate(double Latitude, double Longitude);
var london = new Coordinate(51.5074, -0.1278);
var paris = new Coordinate(48.8566, 2.3522);
// Value type — copied on assignment (no shared references)
var copy = london;
// copy and london are independent — changing one does not affect the other
Records vs Classes vs Structs
| Type | Equality | Mutability | Best For |
|---|---|---|---|
class |
Reference | Mutable by default | Entities, services, controllers |
record class |
Value (all props) | Immutable by default | DTOs, value objects, domain events |
struct |
Value | Mutable by default | Small value types (Point, Color) |
record struct |
Value (all props) | Immutable by default | Small immutable value types |
Common Mistakes
Mistake 1 — Using a record for an EF Core entity (breaks change tracking)
❌ Wrong — EF Core change tracker uses reference equality:
public record Post(int Id, string Title); // record for EF entity — avoid!
✅ Correct — use a class for EF Core entities, records for DTOs.
Mistake 2 — Mutating a positional record property directly
❌ Wrong — init-only: compile error after object construction:
var p = new Point(1, 2);
p.X = 5; // compile error — init-only property
✅ Correct — use the with expression to create a modified copy:
var updated = p with { X = 5 }; // ✓ new Point(5, 2)