C# Records Explained: A Guide to Immutable Data Types

An introduction to C# records. Learn what they are, why they are useful for creating immutable data types, and how their concise syntax can make your code cleaner and more robust.

Introduced in C# 9, records provide a new, simplified syntax for creating immutable reference types. They are designed to solve a common problem: creating simple data transfer objects (DTOs) or models whose primary purpose is to store data.

While you could always create these with classes, it involved a lot of boilerplate code to handle immutability, equality, and string representation. Records automate all of this, allowing you to write cleaner and more robust code.

What is a Record?

A record is a special kind of class that is optimized for storing data. By default, records are immutable, which means their properties cannot be changed after the object has been created. This makes them perfect for representing data that shouldn't be modified, which can help prevent a whole class of bugs in your applications.

The Concise Syntax: Positional Records

The most striking feature of records is their concise declaration syntax, known as positional records.

The Old Way (with a class):

public class User
{
    public int Id { get; }
    public string Username { get; }

    public User(int id, string username)
    {
        Id = id;
        Username = username;
    }

    // ... plus you'd need to override Equals, GetHashCode, and ToString
    // for proper value-based equality and nice printing.
}

The New Way (with a record):

public record User(int Id, string Username);

That one line of code is (almost) equivalent to the entire class definition above, but it also gives you much more for free.

What Do You Get for Free?

When you declare a record, the C# compiler automatically generates several useful methods for you:

  1. A Primary Constructor: The parameters in the declaration (int Id, string Username) become a constructor.

  2. Public init-only Properties: Properties are created for each constructor parameter. They are init-only, which means they can only be set during object initialization, making the object immutable.

  3. Value-Based Equality: This is a huge benefit. Two record instances are considered equal if all their properties are equal. With classes, two instances are only equal if they are the same instance in memory.

    var user1 = new User(1, "alice");
    var user2 = new User(1, "alice");
    
    Console.WriteLine(user1 == user2); // Output: True
    

    If User were a class, this would be False.

  4. A Clean ToString() Override: Records automatically generate a ToString() method that gives you a nice, readable representation of the object.

    var user = new User(1, "alice");
    Console.WriteLine(user.ToString()); // Output: User { Id = 1, Username = alice }
    
  5. A Deconstructor: This allows you to easily unpack a record's properties into separate variables.

    var (userId, username) = user;
    Console.WriteLine(username); // Output: alice
    

Non-Destructive Mutation with the with Expression

Because records are immutable, you can't change their properties. But what if you need to create a new instance that is a copy of an existing one, but with one property changed? This is called non-destructive mutation, and records have a special with expression for it.

var user1 = new User(1, "alice");

// Create a new User instance that is a copy of user1, but with a new username
var user2 = user1 with { Username = "bob" };

Console.WriteLine(user1.Username); // Output: alice (user1 is unchanged)
Console.WriteLine(user2.Username); // Output: bob

This is a powerful and safe way to work with immutable data.

When to Use Records

Records are the perfect choice when your primary goal is to store data. They are ideal for:

  • Data Transfer Objects (DTOs): For transferring data between layers of your application or over an API.
  • Models in Functional-Style Programming: Their immutability makes them a great fit for a more functional programming style.
  • Dictionary Keys: Because they have value-based equality and hash code generation, they can be safely used as keys in a dictionary.

When to Still Use Classes

Classes are still the right choice when you need to model an entity with a distinct identity and behavior that changes over its lifetime. If you have an object that represents a connection to a database or a file stream, that's a class. If you have an object that simply represents the data for a user, that's a record.

Conclusion

C# records are a fantastic addition to the language that significantly reduce boilerplate and encourage the use of immutable types. By providing value-based equality, a clean ToString() implementation, and support for non-destructive mutation out of the box, they allow you to write code that is more concise, more readable, and less prone to bugs.