C# Records Explained: Immutable Data Models and Value-Based Equality

Records are a modern C# feature designed for working with data-focused objects. They provide convenient value-based equality, concise syntax, readable object creation, and useful support for immutable data.

What is a Record in C#?

A record is a reference type by default that is designed primarily to represent data. Unlike traditional classes, records compare their values rather than only comparing object references.

CSHARP
public record Person(string Name, int Age);

Person person = new Person("Alice", 25);

Console.WriteLine(person);

Why Use Records?

  • Represent data-focused objects clearly
  • Provide value-based equality
  • Reduce boilerplate code
  • Support immutable-style programming
  • Make data transfer models easier to define
  • Provide convenient non-destructive object updates

Record Positional Syntax

The positional record syntax allows properties and the primary constructor to be declared in a single concise statement.

CSHARP
public record Product(
    int Id,
    string Name,
    decimal Price
);

Product product = new Product(101, "Laptop", 75000m);

Value-Based Equality

Two record instances containing the same values are considered equal. This differs from the default equality behavior of ordinary classes.

CSHARP
Person first = new Person("Alice", 25);
Person second = new Person("Alice", 25);

Console.WriteLine(first == second);

public record Person(string Name, int Age);

Records vs Classes

FeatureRecordClass
Primary PurposeData-oriented modelingGeneral object-oriented behavior
EqualityValue-based by defaultReference-based by default
Concise SyntaxYesUsually requires more code
ImmutabilityEasy to modelMust be designed explicitly
Common UseDTOs and data modelsServices and complex behavior

Immutable Properties

Positional records expose init-only properties, which means their values can normally be assigned during object creation or initialization but not changed through ordinary property assignment afterward.

CSHARP
public record User(string Name, string Email);

User user = new User("John", "john@example.com");

// user.Name = "David";
// Not allowed after initialization

The with Expression

The with expression creates a new record instance based on an existing instance while changing selected properties.

CSHARP
public record User(string Name, string Role);

User user = new User("Alice", "Developer");

User updatedUser = user with
{
    Role = "Senior Developer"
};

Console.WriteLine(user.Role);
Console.WriteLine(updatedUser.Role);

Record Inheritance

Record classes can participate in inheritance, allowing related data models to share common members.

CSHARP
public record Person(string Name);

public record Employee(
    string Name,
    int EmployeeId
) : Person(Name);

Employee employee = new Employee("Alice", 1001);

Record Classes

The record class syntax makes it explicit that the record is a reference type.

CSHARP
public record class Customer
{
    public int Id { get; init; }
    public string Name { get; init; } = "";
}

Customer customer = new Customer
{
    Id = 1,
    Name = "Alice"
};

Record Structs

C# also supports record structs. Unlike record classes, record structs are value types and can be useful for small data values.

CSHARP
public record struct Point(int X, int Y);

Point point = new Point(10, 20);

Console.WriteLine(point);

Record Struct vs Record Class

FeatureRecord ClassRecord Struct
TypeReference typeValue type
Default EqualityValue-basedValue-based
NullabilityCan be nullCannot normally be null
Typical UseData modelsSmall value-like data

Records and Deconstruction

Positional records provide convenient deconstruction, allowing their values to be assigned to separate variables.

CSHARP
public record Person(string Name, int Age);

Person person = new Person("Alice", 30);

var (name, age) = person;

Console.WriteLine(name);
Console.WriteLine(age);

Records and Pattern Matching

Records work naturally with C# pattern matching, making them useful when applications need to inspect data and perform different operations based on its shape.

CSHARP
public record Order(int Id, decimal Total);

Order order = new Order(1001, 2500m);

if (order is Order { Total: > 1000 })
{
    Console.WriteLine("Large order");
}

Records for DTOs

Records are often useful for data transfer objects because they make the structure of data clear and can provide value-based equality.

CSHARP
public record UserResponse(
    int Id,
    string Name,
    string Email
);

UserResponse response = new UserResponse(
    10,
    "Alice",
    "alice@example.com"
);

Common Record Use Cases

  • API request and response models
  • Configuration data
  • Immutable application data
  • Value objects
  • Messages between application components
  • Data transfer objects
  • Small value-based models

Common Mistakes to Avoid

  • Using records for every type in an application
  • Assuming records make every referenced object deeply immutable
  • Using record structs for large or complex data
  • Changing mutable collections inside otherwise immutable records
  • Choosing records when identity-based behavior is more important than value equality

Record Best Practices

  • Use records for data-centric types
  • Prefer immutable properties when appropriate
  • Use with expressions for non-destructive updates
  • Choose record classes or record structs based on value/reference semantics
  • Keep record models focused on representing data
  • Use normal classes when object identity and behavior are central

Real-World Applications

  • REST API models
  • Application configuration
  • Financial value objects
  • User profile data
  • Event messages
  • Search result models
  • Immutable application state

Practice Exercises

  • Create a Person record
  • Compare two records using value equality
  • Update a record using with
  • Create a record with multiple properties
  • Build a record-based API response model
  • Create a record struct for coordinates
  • Compare records and classes in the same application

Conclusion

C# records provide a concise way to model data while offering value-based equality and convenient immutable-style updates. They are especially useful for DTOs, messages, value objects, and other data-focused types where the contents of an object matter more than its identity.

Note: Note: Records are not a replacement for classes. Choose records when value-oriented data modeling is the main goal and use classes when object identity or complex mutable behavior is more important.