C# 9 Records and Value-Based Equality Explained

This article explores the record type and value-based equality introduced in C# 9.0. You will learn what records are, how they differ from standard classes and structs, how value-based equality functions under the hood, and how features like non-destructive mutation streamline data modeling in modern .NET applications.

What Are Records in C# 9?

Introduced in C# 9, a record is a reference type that provides a concise syntax for defining immutable data models. While traditional classes are primarily designed around object-oriented encapsulation and mutable state, records are intended to treat data as pure values.

You can define a record using positional syntax in a single line:

public record Person(string FirstName, string LastName);

Under the hood, the C# compiler generates:

  • An immutable reference type.
  • Public init-only properties for each positional parameter.
  • A primary constructor to populate those properties.
  • Value-based equality implementations (Equals, GetHashCode, and operator overloads for == and !=).
  • A deconstructor (Deconstruct) method.
  • A copy constructor and cloning mechanism for non-destructive mutation.

Understanding Value-Based Equality

The defining characteristic of records is value-based equality.

By default, C# reference types (classes) use reference equality. Two instances of a class are considered equal only if they point to the exact same location in memory, even if all their property values match.

In contrast, value types (structs) use value equality, but standard structs rely on reflection via ValueType.Equals, which introduces performance overhead.

Records bridge this gap: they are reference types, but the compiler automatically generates strongly typed equality methods that compare the values of their properties instead of memory addresses.

Comparing Classes and Records

Consider how equality behaves in a standard class versus a record:

// Standard Class
public class ClassPerson
{
    public string Name { get; set; }
}

var c1 = new ClassPerson { Name = "Alice" };
var c2 = new ClassPerson { Name = "Alice" };
Console.WriteLine(c1 == c2); // Outputs: False (different memory addresses)

// Record
public record RecordPerson(string Name);

var r1 = new RecordPerson("Alice");
var r2 = new RecordPerson("Alice");
Console.WriteLine(r1 == r2); // Outputs: True (same values)

For two record instances to be considered equal:

  1. Both instances must be of the same runtime type.
  2. Every field and property in both instances must be equal according to their own equality rules.

Built-In Equality Members

When you declare a record, the compiler synthesizes the following equality contracts automatically:

  • IEquatable<T>: Provides high-performance, type-safe equality checking without boxing.
  • virtual bool Equals(object? obj) and virtual bool Equals(R? other): Overridden methods to ensure polymorphic equality works correctly across inheritance hierarchies.
  • GetHashCode(): Overridden so that instances with matching property values yield identical hash codes, making records safe to use as keys in hash tables and dictionaries.
  • == and != operators: Overloaded to invoke the value-equality logic instead of standard reference comparison.

Non-Destructive Mutation with the with Expression

Because records are designed to be immutable, modifying a record requires creating a copy with updated values. C# 9 introduced the with expression to support this non-destructive mutation:

var original = new Person("John", "Doe");
var updated = original with { LastName = "Smith" };

Console.WriteLine(original == updated); // Outputs: False

The with expression uses a compiler-synthesized copy constructor to duplicate the existing instance while altering only the specified properties.

When to Use Records

Records are ideal when:

  • You need data transfer objects (DTOs) or API request/response models.
  • You want immutable data models to prevent unintended side effects.
  • You need objects compared by their values rather than their identity.
  • You want concise, readable declarations for data-centric structures.