DEV Community

Cover image for Record Types in C# — Immutability, Equality, and When to Use Them
Libin Tom Baby
Libin Tom Baby

Posted on

Record Types in C# — Immutability, Equality, and When to Use Them

record, record struct, init, with expressions, value equality

Record Types in C#

Records were introduced in C# 9 and enhanced in C# 10.

They look like classes. They behave differently.

Most developers use records without fully understanding what makes them special — and more importantly, when they're the right tool and when they're not.

This guide breaks down everything you need to know.


What is a Record?

A record is a reference type (like a class) that is designed for immutable data with value-based equality.

public record Person(string Name, int Age);
Enter fullscreen mode Exit fullscreen mode

That one line gives you:

  • A constructor
  • Public init-only properties
  • ToString() override
  • Equals() and GetHashCode() based on values — not references
  • Deconstruction support
  • with expression support

Value Equality — The Core Difference

With a class, two objects are equal only if they are the same object in memory.

With a record, two objects are equal if their values are the same.

Class (reference equality)

var p1 = new PersonClass { Name = "Alice", Age = 30 };
var p2 = new PersonClass { Name = "Alice", Age = 30 };

Console.WriteLine(p1 == p2); // False — different objects in memory
Enter fullscreen mode Exit fullscreen mode

Record (value equality)

var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);

Console.WriteLine(p1 == p2); // True — same values
Enter fullscreen mode Exit fullscreen mode

This is the single most important thing to understand about records.


Immutability With init

Record properties use init accessors by default — they can only be set during construction.

var person = new Person("Alice", 30);
person.Name = "Bob"; // ❌ Compile error — init-only
Enter fullscreen mode Exit fullscreen mode

This makes records safe to pass around without defensive copying.


The with Expression

You cannot mutate a record. But you can create a modified copy using with.

var original = new Person("Alice", 30);
var updated = original with { Age = 31 };

Console.WriteLine(original); // Person { Name = Alice, Age = 30 }
Console.WriteLine(updated);  // Person { Name = Alice, Age = 31 }
Enter fullscreen mode Exit fullscreen mode

The original is untouched.
with creates a shallow copy with the specified properties replaced.


Deconstruction

Records support positional deconstruction automatically.

public record Person(string Name, int Age);

var person = new Person("Alice", 30);
var (name, age) = person;

Console.WriteLine(name); // Alice
Console.WriteLine(age);  // 30
Enter fullscreen mode Exit fullscreen mode

Record vs Class — Feature Comparison

Feature Record Class
Type Reference type Reference type
Equality Value-based Reference-based
Immutability init by default Mutable by default
with expression Yes No
Inheritance Supported Supported
ToString() Auto-generated Default object.ToString()
Best for DTOs, value objects, API responses Entities, services, stateful objects

Record Struct (C# 10)

C# 10 introduced record struct — a value type record.

public record struct Point(double X, double Y);

var a = new Point(1.0, 2.0);
var b = new Point(1.0, 2.0);

Console.WriteLine(a == b); // True — value equality
Enter fullscreen mode Exit fullscreen mode

record struct gives you:

  • Value type semantics (stack allocation, no GC pressure)
  • Value equality
  • with expression
  • No heap allocation

Use record struct for small, frequently used, immutable value objects.


When to Use Records

Use records for

  • DTOs (Data Transfer Objects) — data flowing between layers
  • API request/response models
  • Value objects in Domain-Driven Design
  • Query results that should not be mutated
  • Configuration objects
  • Any data where equality means "same values"

Do NOT use records for

  • Domain entities that have identity (an Order with an ID is not equal to another Order with the same values — they are separate things)
  • Services and repositories — these have behaviour, not just data
  • Objects with complex mutable state

Real-World Example

// ✅ Perfect use of record — API response DTO
public record OrderResponse(
    Guid Id,
    string CustomerName,
    decimal TotalAmount,
    DateTime CreatedAt
);

// ✅ With expression for building variations in tests
var baseOrder = new OrderResponse(Guid.NewGuid(), "Alice", 150.00m, DateTime.UtcNow);
var updatedOrder = baseOrder with { TotalAmount = 200.00m };

// ✅ Value equality in assertions
var expected = new OrderResponse(id, "Alice", 150.00m, createdAt);
var actual = service.GetOrder(id);

Assert.Equal(expected, actual); // Works because of value equality
Enter fullscreen mode Exit fullscreen mode

Record Inheritance

Records support inheritance — but with one rule:

A record can only inherit from another record (not a class).

public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);

var dog = new Dog("Rex", "Labrador");
Console.WriteLine(dog); // Dog { Name = Rex, Breed = Labrador }
Enter fullscreen mode Exit fullscreen mode

Equality checks include all derived type properties.


Interview-Ready Summary

  • Records are reference types with value-based equality
  • Two records with the same values are considered equal
  • Properties are init-only by default — immutable after construction
  • Use with to create modified copies without mutating the original
  • record struct is a value type record — no heap allocation
  • Use records for DTOs, API models, value objects
  • Do not use records for domain entities that have identity

A strong interview answer:

"Records are designed for immutable data with value-based equality. Unlike classes where equality means same reference, two records with the same values are equal. The with expression lets you produce modified copies non-destructively. They're ideal for DTOs and value objects, but not for domain entities where identity matters more than content."


Add-On — Records in Pattern Matching

Records work exceptionally well with C# pattern matching.

public record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;

double GetArea(Shape shape) => shape switch
{
    Circle(var r)          => Math.PI * r * r,
    Rectangle(var w, var h) => w * h,
    _                      => throw new ArgumentException("Unknown shape")
};
Enter fullscreen mode Exit fullscreen mode

Positional patterns work directly with record deconstruction — no extra code needed.


Top comments (0)