Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

A record in C# is a type declaration modifier that instructs the compiler to synthesize members for value-based equality, non-destructive mutation, and formatted string representation. By default, a record is a reference type (implicitly a record class), but it can also be declared as a value type (record struct).

Syntax Variations

Positional Syntax Declaring a record with a primary constructor automatically generates public, init-only properties (for reference types) or mutable properties (for record struct types).
public record Person(string FirstName, string LastName);
Nominal Syntax Records can be declared using standard property syntax, allowing for explicit control over mutability and access modifiers.
public record Product
{
    public string Name { get; set; }
    public decimal Price { get; init; }
}
Record Structs Applying the record modifier to a struct provides the same synthesized members for value types. Positional record struct properties are mutable by default unless explicitly marked readonly.
public readonly record struct Point(double X, double Y);

Compiler-Synthesized Members

When a record is compiled, the C# compiler automatically generates several members to enforce its behavior: 1. Value Equality (IEquatable<T>) The compiler overrides Object.Equals(Object) and implements IEquatable<T>.Equals(T). Two record instances are equal if they share the exact same run-time type and all their property and field values are equal. The compiler also overloads the == and != operators. Note: Equality is shallow. If a record contains reference type properties (such as arrays, lists, or other classes), equality checks will only compare the memory addresses (references) of those objects, not their underlying data.
var p1 = new Person("John", "Doe");
var p2 = new Person("John", "Doe");
bool isEqual = p1 == p2; // Evaluates to true
2. Hash Code Generation The compiler overrides GetHashCode(), computing a hash based on the values of all instance fields and properties. This ensures that records with identical data produce identical hash codes, making them safe for use as keys in hash-based collections like Dictionary<TKey, TValue>. 3. Non-Destructive Mutation (with Expression) Records support the with expression, which creates a new instance of the record with specified properties modified. Note: Mutation is shallow. Reference type properties are copied by reference rather than deeply cloned. Modifying the internal state of a referenced object in the new record will affect the original record. The compiler implements this differently depending on the record type:
  • record class: The compiler generates a hidden virtual <Clone>$ method and a protected copy constructor to instantiate the new object.
  • record struct: The compiler relies on standard value-type assignment (a bitwise memory copy) and does not generate a <Clone>$ method or a copy constructor.
var p3 = p1 with { LastName = "Smith" };
// p1 remains ("John", "Doe"), p3 is ("John", "Smith")
4. Formatting (ToString and PrintMembers) The compiler overrides ToString() to output a formatted string containing the type name and the names and values of its public properties. To handle the string construction, it generates a PrintMembers method:
  • record class: Generates a protected virtual PrintMembers method, which can be overridden in derived records to customize the output.
  • record struct: Because structs cannot participate in inheritance and cannot have protected or virtual members, the compiler generates a private, non-virtual PrintMembers method.
Console.WriteLine(p1); 
// Output: Person { FirstName = John, LastName = Doe }
5. Deconstruction If positional syntax is used, the compiler generates a Deconstruct method, allowing the record’s properties to be unpacked directly into individual variables.
var (first, last) = p1;

Inheritance Rules and Equality Contracts

  • A record class can inherit from another record class, but it cannot inherit from a standard class.
  • A standard class cannot inherit from a record class.
  • A record struct cannot participate in inheritance (as structs in C# are implicitly sealed), but it can implement interfaces.
  • To maintain symmetric equality across inheritance hierarchies, the compiler generates a virtual EqualityContract property that returns the Type object of the record. When evaluating equality, the EqualityContract of both operands must match. Therefore, a base record and a derived record with identical property values will evaluate as unequal.
Master C# with Deep Grasping Methodology!Learn More