record (C#)
A type introduced in C# 9 designed specifically for holding data. It is designed as an immutable object and provides compact syntax for copy-creation with with expressions, value-based equality comparison, and deconstruction via Deconstruct.
Syntax
record TypeName(Type PropertyName, ...);
// Block-form record (allows defining additional members).
record TypeName(Type PropertyName, ...)
{
// Additional properties and methods can be defined here.
}
// with expression: copies the record while changing selected properties.
TypeName newVar = originalVar with { PropertyName = newValue };
// record struct (value-type record; C# 10 and later).
record struct TypeName(Type PropertyName, ...);
Key Features of record
| Feature | Description |
|---|---|
| Immutable properties (init-only) | Properties of a positional record are generated with an init accessor, so they cannot be changed after initialization. |
| Value-based equality | Two records are considered equal with == when their property values match, not their references. |
| with expression | Creates a copy of the original object with only the specified properties changed, leaving the original untouched. |
| Deconstruct | A positional record automatically generates a deconstruction method, allowing its values to be unpacked into variables like a tuple. |
| Auto-generated ToString() | Automatically returns a string containing the type name along with each property name and value. |
| record struct | A value-type record. No heap allocation is required; it is placed on the stack (C# 10 and later). |
record vs. class Comparison
| Item | record (reference type) | class |
|---|---|---|
| Equality (==) | Value-based (property contents) | Reference-based (same instance) |
| Immutability | Automatically immutable via init accessor for positional records. | You must design readonly fields or properties yourself. |
| with expression | Supported. | Not supported. |
| Deconstruct | Auto-generated for positional records. | Must be defined manually. |
| ToString() | Auto-generated (type name + properties) | Default returns type name only. |
| Inheritance | A record can only inherit from another record. | A class can inherit from another class. |
Sample Code
RecordBasic.cs
using System;
// The compiler automatically generates a constructor, init-only properties, Deconstruct, ToString, and ==.
record Member(string Name, string Level, int Score);
class RecordBasic {
static void Main() {
// Creates instances of the record.
var member_2 = new Member("member_2", "A", 80000);
var member_1 = new Member("member_1", "S", 999999);
var member_3 = new Member("member_3", "A", 75000);
// ToString() is auto-generated.
Console.WriteLine(member_2);
// → Member { Name = member_2, Level = A, Score = 80000 }
// Two instances with the same property values are considered equal with ==.
var member_2b = new Member("member_2", "A", 80000);
Console.WriteLine(member_2 == member_2b); // True
Console.WriteLine(member_2 == member_1); // False
var member_2_updated = member_2 with { Score = 999999 };
Console.WriteLine(member_2_updated);
// → Member { Name = member_2, Level = A, Score = 999999 }
// The original instance is unchanged (immutability).
Console.WriteLine(member_2.Score); // 80000
var (name, level, score) = member_3;
Console.WriteLine($"{name} / {level} / Score: {score}");
// → member_3 / A / Score: 75000
}
}
This produces the following output:
dotnet script RecordBasic.cs
Member { Name = member_2, Level = A, Score = 80000 }
True
False
Member { Name = member_2, Level = A, Score = 999999 }
80000
member_3 / A / Score: 75000
RecordBlock.cs
using System;
// A block-form record. In addition to positional properties, additional members can be defined.
record Project(string Owner, string Category)
{
// Adds a computed property (can be written in block-form records).
public string Description
=> $"{Owner}'s project: {Category}";
// Custom methods can also be defined.
public bool IsCritical()
{
// Treats a category containing "urgent" as a critical project.
return Category.Contains("urgent");
}
}
class RecordBlock {
static void Main() {
var projA = new Project("member_1", "review (urgent)");
var projB = new Project("member_3", "research");
var projC = new Project("member_6", "deploy (urgent)");
Console.WriteLine(projA.Description);
// → member_1's project: review (urgent)
Console.WriteLine($"projA is critical: {projA.IsCritical()}"); // True
Console.WriteLine($"projB is critical: {projB.IsCritical()}"); // False
Console.WriteLine($"projC is critical: {projC.IsCritical()}"); // True
// The with expression also works with block-form records.
var projBUpdated = projB with { Category = "research (urgent)" };
Console.WriteLine($"Updated: {projBUpdated.Description}");
// → member_3's project: research (urgent)
}
}
This produces the following output:
dotnet script RecordBlock.cs member_1's project: review (urgent) projA is critical: True projB is critical: False projC is critical: True Updated: member_3's project: research (urgent)
RecordVsRecordStruct.cs
using System;
// record (reference type): allocated on the heap.
record TeamA(string Name, int Rank);
// record struct (value type): allocated on the stack (C# 10 and later).
// Positional properties are mutable by default (a set accessor is generated).
record struct TeamB(string Name, int Rank);
class RecordVsRecordStruct {
static void Main() {
var entryA1 = new TeamA("item_a", 20);
var entryA2 = new TeamA("item_a", 20);
// Value-based comparison (reference-type records compare by property values).
Console.WriteLine(entryA1 == entryA2); // True
var entryB1 = new TeamB("item_b", 16);
var entryB2 = new TeamB("item_b", 16);
// record struct also uses value-based comparison.
Console.WriteLine(entryB1 == entryB2); // True
// Properties of a record struct can be modified (unlike a reference-type record).
entryB1.Rank = 18; // record struct is mutable by default.
Console.WriteLine(entryB1); // TeamB { Name = item_b, Rank = 18 }
// The with expression also works with record struct.
var entryB3 = new TeamB("item_c", 19);
var entryB3Modified = entryB3 with { Rank = 15 };
Console.WriteLine(entryB3Modified); // TeamB { Name = item_c, Rank = 15 }
// The original entryB3 is unchanged.
Console.WriteLine(entryB3); // TeamB { Name = item_c, Rank = 19 }
}
}
This produces the following output:
dotnet script RecordVsRecordStruct.cs
True
True
TeamB { Name = item_b, Rank = 18 }
TeamB { Name = item_c, Rank = 15 }
TeamB { Name = item_c, Rank = 19 }
Common Mistakes
Directly assigning to an init-only property
Properties of a positional record are generated with an init accessor, so attempting to change a value after initialization causes a compile error. Use a with expression to create a new instance with the desired changes.
record Member(string Name, int Score);
var entry = new Member("item_x", 9000);
// Compile error: an init-only property cannot be modified after initialization.
// entry.Score = 15000;
var entryUpdated = entry with { Score = 15000 };
Mutability difference between record and record struct
A reference-type record is init-only (immutable), but record struct properties have a set accessor by default, making them mutable. To make them immutable, declare the type as readonly record struct.
record RefRecord(string Name, int Value);
record struct MutableStruct(string Name, int Value);
readonly record struct ImmutableStruct(string Name, int Value);
var r = new RefRecord("A", 1);
// r.Value = 2; // Compile error: init-only.
var ms = new MutableStruct("B", 2);
ms.Value = 3; // record struct is mutable by default.
var ims = new ImmutableStruct("C", 3);
// ims.Value = 4; // Compile error: readonly record struct is immutable.
Notes
record is a type designed primarily for holding data. With positional syntax, the compiler automatically generates the constructor, init-only properties, Deconstruct, ToString, and equality comparison — significantly reducing boilerplate compared to an equivalent class.
The with expression is used to create a copy that differs in only select properties without modifying the original object. Directly assigning to an init-only property (e.g., member_2.Score = 999;) is a compile error. Always use a with expression when you need a modified copy.
Because record struct is a value type, it is allocated on the stack with no heap allocation. However, positional record struct properties are mutable (set accessor), which differs from the immutability of a reference-type record. To make it immutable, declare it as readonly record struct.
For type checking and safe casting, see is / as / Pattern Matching. For use with nullable types, see Nullable<T> / Nullable Types.
If you find any errors or copyright issues, please contact us.