Records — Immutable Data Types and Value Semantics

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
Note: Record equality compares all properties by value — this is fundamentally different from class equality where two distinct objects are never equal unless you override 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.
Tip: Use the 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.
Warning: Do not use record classes for EF Core entity models if you rely on change tracking. EF Core’s change tracker uses reference equality to identify tracked entities — two record instances with identical values would appear to EF Core as the same entity even if they are different database rows. Use regular classes for EF Core entities. Use records for the DTOs/view models that carry data between your controller and service layers, and for value objects within the domain layer.

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)

🧠 Test Yourself

Given record Money(decimal Amount, string Currency);, will new Money(100m, "USD") == new Money(100m, "USD") return true? What about for a class with the same two properties?