C# Records Explained: Why and How to Use Them

One of the most exciting features added to modern C# (starting with C# 9.0) is the record type. Records are designed to make your code more concise, readable, and immutable-friendly — perfect for modern application development.

If you’ve ever written a class just to hold data, overridden Equals, GetHashCode, and ToString manually, or struggled with immutability, you’ll appreciate what records bring to the table.

In this article, we’ll cover everything you need to know about records in C#:

  • What a record is (and how it differs from a class)

  • Why records were added to the language

  • How to declare and use records

  • Value equality vs reference equality

  • Positional records and concise syntax

  • Inheritance and immutability with records

  • Real-world use cases

  • Performance considerations and trade-offs

  • Best practices for using records effectively

What Is a Record in C#?

A record is a special kind of reference type in C# designed to represent immutable data models. It’s similar to a class in many ways but with some important differences:

  • Records provide value-based equality by default.

  • Records emphasize immutability.

  • Records come with concise syntax for declaring properties.

Think of records as the “go-to” tool when you’re modeling data rather than behavior.

Classes vs Records: The Big Difference

Let’s start with a simple comparison.

Using a Class

A typical “Person” class

If you create two instances with the same values:

The equality operator compares object reference IDs by default- not the values of the object, as many people would guess

Even though the data is the same, they’re considered different objects.

Using a Record

Notice the “record” keyword, where you would normally use “class”

Now if you compare:

The records are comparing objects based their values

By default, records are compared by values of their properties — not by memory reference.

Why Records Were Added to C#

Before records, if you wanted this behavior with classes, you had to write boilerplate code:

  • Override Equals()

  • Override GetHashCode()

  • Override ToString()

This was tedious and error-prone. Records give you all of that for free with one line of code.

Declaring Records

There are two main ways to declare records:

1. Positional Records (Concise Syntax)

Concise syntax for positional records

This:

  • Creates immutable properties (Name, Age)

  • Provides a constructor

  • Implements Equals, GetHashCode, and ToString

Example:

Using the Person record

2. Records with Property Declarations

A “Product” record with properties

This is closer to a traditional class style but still gets the benefits of records.

Init-Only Properties

Notice the use of init instead of set?

That means:

Note that the price cannot be modified after creation

Records encourage immutability: once created, the object’s data shouldn’t change.

Value Equality in Action

The key difference: records compare data, not references.

Comparison with records

Whereas with classes:

Class objects compare by object reference ID number by default, not considering 2 “people” the same just because they have the same name and age

With-Expressions

Another powerful feature of records: non-destructive mutation.

Using a “with-expression” to make a copy of a Person record, for. a new Person

This lets you “copy and modify” without changing the original object — super useful in immutable programming patterns.

Inheritance with Records

Records can inherit from other records:

Demonstrating inheritance with records

Example:

Creating a Dog record, derived from Animal

⚠️ However: records can only inherit from other records (not classes).

Real-World Use Cases for Records

1. DTOs (Data Transfer Objects)

When sending/receiving data via APIs, records are ideal for modeling payloads.

Example of a DTO record

2. Immutable State in Applications

Useful in functional or reactive programming. For example, in Redux-like state management systems:

Use records with functional programming

3. Pattern Matching

Records work beautifully with switch expressions:

An example with a switch statement

Performance Considerations

Records are still reference types (like classes), so:

  • They live on the heap (not stack).

  • They’re garbage-collected.

  • They’re not as lightweight as structs.

If you need ultra-low overhead (e.g., high-performance gaming loops), use readonly struct.
If you need value equality + immutability + conciseness, use record.

Best Practices

  1. Use records for immutable data models
    DTOs, view models, messages, config objects.

  2. Don’t use records for mutable business objects
    If you need setters and business logic, classes are still the right tool.

  3. Prefer positional records for simple models
    Cleaner, more concise.

  4. Use with for copying/mutating safely
    It makes code more readable and consistent.

Conclusion

Records in C# are a game-changer for data modeling. They combine conciseness, immutability, and value-based equality in a way that makes code simpler and safer.

You don’t need to write repetitive boilerplate for Equals, GetHashCode, or ToString. You get immutable data patterns out of the box. And you can model data in a way that’s closer to functional programming while still leveraging all the power of C#.

Whenever you’re defining a type whose main purpose is holding data, ask yourself:
👉 “Should this be a record instead of a class?”

Chances are, the answer is yes.

Join us at our Skool community to keep the discussion going.

Next
Next

Reflection in C#: What It Is, How It Works, and Why It Matters