Records (C# reference)
Beginning with C# 9, you use the record keyword to define a reference type that provides built-in functionality for encapsulating data. You can create record types with immutable properties by using positional parameters or standard property syntax:
public record Person(string FirstName, string LastName);
public record Person
{
public string FirstName { get; init; } = default!;
public string LastName { get; init; } = default!;
};
You can also create record types with mutable properties and fields:
public record Person
{
public string FirstName { get; set; } = default!;
public string LastName { get; set; } = default!;
};
While records can be mutable, they're primarily intended for supporting immutable data models. The record type offers the following features:
- Concise syntax for creating a reference type with immutable properties
- Built-in behavior useful for a data-centric reference type:
- Support for inheritance hierarchies
You can also use structure types to design data-centric types that provide value equality and little or no behavior. In C# 10 and later, you can define record struct types using either positional parameters, or standard property syntax:
public readonly record struct Point(double X, double Y, double Z);
public record struct Point
{
public double X { get; init; }
public double Y { get; init; }
public double Z { get; init; }
}
Record structs can be mutable as well, both positional record structs and record structs with no positional parameters:
public record struct DataMeasurement(DateTime TakenAt, double Measurement);
public record struct Point
{
public double X { get; set; }
public double Y { get; set; }
public double Z { get; set; }
}
The preceding examples show some distinctions between records that are reference types and records that are value types:
- A
recordor arecord classdeclares a reference type. Theclasskeyword is optional, but can add clarity for readers. Arecord structdeclares a value type. - Positional properties are immutable in a
record classand areadonly record struct. They're mutable in arecord struct.
The remainder of this article discusses both record class and record struct types. The differences are detailed in each section. You should decide between a record class and a record struct similar to deciding between a class and a struct. The term record is used to describe behavior that applies to all record types. Either record struct or record class is used to describe behavior that applies to only struct or class types, respectively.
Positional syntax for property definition
You can use positional parameters to declare properties of a record and to initialize the property values when you create an instance:
public record Person(string FirstName, string LastName);
public static void Main()
{
Person person = new("Nancy", "Davolio");
Console.WriteLine(person);
// output: Person { FirstName = Nancy, LastName = Davolio }
}
When you use the positional syntax for property definition, the compiler creates:
- A public init-only auto-implemented property for each positional parameter provided in the record declaration.
- For
recordtypes andreadonly record structtypes: An init-only property can only be set in the constructor or by using a property initializer. - For
record structtypes: A read-write property that can be set in a constructor, property initializer, or assignment after construction.
- For
- A primary constructor whose parameters match the positional parameters on the record declaration.
- For record struct types, a parameterless constructor that sets each field to its default value.
- A
Deconstructmethod with anoutparameter for each positional parameter provided in the record declaration. This method is provided only if there are two or more positional parameters. The method deconstructs properties defined by using positional syntax; it ignores properties that are defined by using standard property syntax.
You may want to add attributes to any of these elements the compiler creates from the record definition. You can add a target to any attribute you apply to the positional record's properties. The following example applies the System.Text.Json.Serialization.JsonPropertyNameAttribute to each property of the Person record. The property: target indicates that the attribute is applied to the compiler-generated property. Other values are field: to apply the attribute to the field, and param: to apply the attribute to the parameter.
/// <summary>
/// Person record type
/// </summary>
/// <param name="FirstName">First Name</param>
/// <param name="LastName">Last Name</param>
/// <remarks>
/// The person type is a positional record containing the
/// properties for the first and last name. Those properties
/// map to the JSON elements "firstName" and "lastName" when
/// serialized or deserialized.
/// </remarks>
public record Person([property: JsonPropertyName("firstName")]string FirstName,
[property: JsonPropertyName("lastName")]string LastName);
The preceding example also shows how to create XML documentation comments for the record. You can add the <param> tag to add documentation for the primary constructor's parameters.
If the generated auto-implemented property definition isn't what you want, you can define your own property of the same name. If you do that, the generated constructor and deconstructor will use your property definition. For instance, the following example declares the FirstName and LastName properties of a positional record public, but restricts the Id positional parameter to internal. You can use this syntax for records and record struct types. You must add the explicit assignment of the declared property to its corresponding positional parameter.
public record Person(string FirstName, string LastName, string Id)
{
internal string Id { get; init; } = Id;
}
public static void Main()
{
Person person = new("Nancy", "Davolio", "12345");
Console.WriteLine(person.FirstName); //output: Nancy
}
A record type doesn't have to declare any positional properties. You can declare a record without any positional properties, and you can declare other fields and properties, as in the following example:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; } = Array.Empty<string>();
};
If you define properties by using standard property syntax but omit the access modifier, the properties are implicitly private.
Immutability
A positional record and a positional readonly record struct declare init-only properties. A positional record struct declares read-write properties. You can override either of those defaults, as shown in the previous section.
Immutability can be useful when you need a data-centric type to be thread-safe or you're depending on a hash code remaining the same in a hash table. Immutability isn't appropriate for all data scenarios, however. Entity Framework Core, for example, doesn't support updating with immutable entity types.
Init-only properties, whether created from positional parameters (record class, and readonly record struct) or by specifying init accessors, have shallow immutability. After initialization, you can't change the value of value-type properties or the reference of reference-type properties. However, the data that a reference-type property refers to can be changed. The following example shows that the content of a reference-type immutable property (an array in this case) is mutable:
public record Person(string FirstName, string LastName, string[] PhoneNumbers);
public static void Main()
{
Person person = new("Nancy", "Davolio", new string[1] { "555-1234" });
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-1234
person.PhoneNumbers[0] = "555-6789";
Console.WriteLine(person.PhoneNumbers[0]); // output: 555-6789
}
The features unique to record types are implemented by compiler-synthesized methods, and none of these methods compromises immutability by modifying object state. Unless specified, the synthesized methods are generated for record, record struct, and readonly record struct declarations.
Value equality
For any type you define, you can override Object.Equals(Object), and overload operator ==. If you don't override Equals or overload operator ==, the type you declare governs how equality is defined:
- For
classtypes, two objects are equal if they refer to the same object in memory. - For
structtypes, two objects are equal if they are of the same type and store the same values. - For
recordtypes, includingrecord structandreadonly record struct, two objects are equal if they are of the same type and store the same values.
The definition of equality for a record struct is the same as for a struct. The difference is that for a struct, the implementation is in ValueType.Equals(Object) and relies on reflection. For records, the implementation is compiler synthesized and uses the declared data members.
Reference equality is required for some data models. For example, Entity Framework Core depends on reference equality to ensure that it uses only one instance of an entity type for what is conceptually one entity. For this reason, records and record structs aren't appropriate for use as entity types in Entity Framework Core.
The following example illustrates value equality of record types:
public record Person(string FirstName, string LastName, string[] PhoneNumbers);
public static void Main()
{
var phoneNumbers = new string[2];
Person person1 = new("Nancy", "Davolio", phoneNumbers);
Person person2 = new("Nancy", "Davolio", phoneNumbers);
Console.WriteLine(person1 == person2); // output: True
person1.PhoneNumbers[0] = "555-1234";
Console.WriteLine(person1 == person2); // output: True
Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
}
To implement value equality, the compiler synthesizes the following methods:
An override of Object.Equals(Object).
This method is used as the basis for the Object.Equals(Object, Object) static method when both parameters are non-null.
A virtual
Equalsmethod whose parameter is the record type. This method implements IEquatable<T>.An override of Object.GetHashCode().
Overrides of operators
==and!=.
You can write your own implementations to replace any of these synthesized methods. If a record type has a method that matches the signature of any synthesized method, the compiler doesn't synthesize that method.
If you provide your own implementation of Equals in a record type, provide an implementation of GetHashCode also.
Nondestructive mutation
If you need to copy an instance with some modifications, you can use a with expression to achieve nondestructive mutation. A with expression makes a new record instance that is a copy of an existing record instance, with specified properties and fields modified. You use object initializer syntax to specify the values to be changed, as shown in the following example:
public record Person(string FirstName, string LastName)
{
public string[] PhoneNumbers { get; init; }
}
public static void Main()
{
Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
Console.WriteLine(person1);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
Person person2 = person1 with { FirstName = "John" };
Console.WriteLine(person2);
// output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { PhoneNumbers = new string[1] };
Console.WriteLine(person2);
// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
Console.WriteLine(person1 == person2); // output: False
person2 = person1 with { };
Console.WriteLine(person1 == person2); // output: True
}
The with expression can set positional properties or properties created by using standard property syntax. Non-positional properties must have an init or set accessor to be changed in a with expression.
The result of a with expression is a shallow copy, which means that for a reference property, only the reference to an instance is copied. Both the original record and the copy end up with a reference to the same instance.
To implement this feature for record class types, the compiler synthesizes a clone method and a copy constructor. The virtual clone method returns a new record initialized by the copy constructor. When you use a with expression, the compiler creates code that calls the clone method and then sets the properties that are specified in the with expression.
If you need different copying behavior, you can write your own copy constructor in a record class. If you do that, the compiler won't synthesize one. Make your constructor private if the record is sealed, otherwise make it protected. The compiler doesn't synthesize a copy constructor for record struct types. You can write one, but the compiler won't generate calls to it for with expressions. Instead, the compiler uses assignment.
You can't override the clone method, and you can't create a member named Clone in any record type. The actual name of the clone method is compiler-generated.
Built-in formatting for display
Record types have a compiler-generated ToString method that displays the names and values of public properties and fields. The ToString method returns a string of the following format:
<record type name> { <property name> = <value>, <property name> = <value>, ...}
For reference types, the type name of the object that the property refers to is displayed instead of the property value. In the following example, the array is a reference type, so System.String[] is displayed instead of the actual array element values:
Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }
To implement this feature, in record class types, the compiler synthesizes a virtual PrintMembers method and a ToString override. In record struct types, this member is private.
The ToString override creates a StringBuilder object with the type name followed by an opening bracket. It calls PrintMembers to add property names and values, then adds the closing bracket. The following example shows code similar to what the synthesized override contains:
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Teacher"); // type name
stringBuilder.Append(" { ");
if (PrintMembers(stringBuilder))
{
stringBuilder.Append(" ");
}
stringBuilder.Append("}");
return stringBuilder.ToString();
}
You can provide your own implementation of PrintMembers or the ToString override. Examples are provided in the PrintMembers formatting in derived records section later in this article. In C# 10 and later, your implementation of ToString may include the sealed modifier, which prevents the compiler from synthesizing a ToString implementation for any derived records. Effectively, that means the ToString output won't include the runtime type information. (All members and values are displayed, because derived records will still have a PrintMembers method generated.)
Inheritance
This section only applies to record class types.
A record can inherit from another record. However, a record can't inherit from a class, and a class can't inherit from a record.
Positional parameters in derived record types
The derived record declares positional parameters for all the parameters in the base record primary constructor. The base record declares and initializes those properties. The derived record doesn't hide them, but only creates and initializes properties for parameters that aren't declared in its base record.
The following example illustrates inheritance with positional property syntax:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}
Equality in inheritance hierarchies
This section applies to record class types, but not record struct types. For two record variables to be equal, the run-time type must be equal. The types of the containing variables might be different. Inherited equality comparison is illustrated in the following code example:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Person student = new Student("Nancy", "Davolio", 3);
Console.WriteLine(teacher == student); // output: False
Student student2 = new Student("Nancy", "Davolio", 3);
Console.WriteLine(student2 == student); // output: True
}
In the example, all variables are declared as Person, even when the instance is a derived type of either Student or Teacher. The instances have the same properties and the same property values. But student == teacher returns False although both are Person-type variables, and student == student2 returns True although one is a Person variable and one is a Student variable. The equality test depends on the runtime type of the actual object, not the declared type of the variable.
To implement this behavior, the compiler synthesizes an EqualityContract property that returns a Type object that matches the type of the record. The EqualityContract enables the equality methods to compare the runtime type of objects when they're checking for equality. If the base type of a record is object, this property is virtual. If the base type is another record type, this property is an override. If the record type is sealed, this property is sealed.
When comparing two instances of a derived type, the synthesized equality methods check all properties of the base and derived types for equality. The synthesized GetHashCode method uses the GetHashCode method from all properties and fields declared in the base type and the derived record type.
with expressions in derived records
The result of a with expression has the same run-time type as the expression's operand. All properties of the run-time type get copied, but you can only set properties of the compile-time type, as the following example shows:
public record Point(int X, int Y)
{
public int Zbase { get; set; }
};
public record NamedPoint(string Name, int X, int Y) : Point(X, Y)
{
public int Zderived { get; set; }
};
public static void Main()
{
Point p1 = new NamedPoint("A", 1, 2) { Zbase = 3, Zderived = 4 };
Point p2 = p1 with { X = 5, Y = 6, Zbase = 7 }; // Can't set Name or Zderived
Console.WriteLine(p2 is NamedPoint); // output: True
Console.WriteLine(p2);
// output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = A, Zderived = 4 }
Point p3 = (NamedPoint)p1 with { Name = "B", X = 5, Y = 6, Zbase = 7, Zderived = 8 };
Console.WriteLine(p3);
// output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = B, Zderived = 8 }
}
PrintMembers formatting in derived records
The synthesized PrintMembers method of a derived record type calls the base implementation. The result is that all public properties and fields of both derived and base types are included in the ToString output, as shown in the following example:
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}
You can provide your own implementation of the PrintMembers method. If you do that, use the following signature:
- For a
sealedrecord that derives fromobject(doesn't declare a base record):private bool PrintMembers(StringBuilder builder); - For a
sealedrecord that derives from another record:protected sealed override bool PrintMembers(StringBuilder builder); - For a record that isn't
sealedand derives from object:protected virtual bool PrintMembers(StringBuilder builder); - For a record that isn't
sealedand derives from another record:protected override bool PrintMembers(StringBuilder builder);
Here's an example of code that replaces the synthesized PrintMembers methods, one for a record type that derives from object, and one for a record type that derives from another record:
public abstract record Person(string FirstName, string LastName, string[] PhoneNumbers)
{
protected virtual bool PrintMembers(StringBuilder stringBuilder)
{
stringBuilder.Append($"FirstName = {FirstName}, LastName = {LastName}, ");
stringBuilder.Append($"PhoneNumber1 = {PhoneNumbers[0]}, PhoneNumber2 = {PhoneNumbers[1]}");
return true;
}
}
public record Teacher(string FirstName, string LastName, string[] PhoneNumbers, int Grade)
: Person(FirstName, LastName, PhoneNumbers)
{
protected override bool PrintMembers(StringBuilder stringBuilder)
{
if (base.PrintMembers(stringBuilder))
{
stringBuilder.Append(", ");
};
stringBuilder.Append($"Grade = {Grade}");
return true;
}
};
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", new string[2] { "555-1234", "555-6789" }, 3);
Console.WriteLine(teacher);
// output: Teacher { FirstName = Nancy, LastName = Davolio, PhoneNumber1 = 555-1234, PhoneNumber2 = 555-6789, Grade = 3 }
}
Note
In C# 10 and later, the compiler will synthesize PrintMembers when a base record has sealed the ToString method. You can also create your own implementation of PrintMembers.
Deconstructor behavior in derived records
The Deconstruct method of a derived record returns the values of all positional properties of the compile-time type. If the variable type is a base record, only the base record properties are deconstructed unless the object is cast to the derived type. The following example demonstrates calling a deconstructor on a derived record.
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
public static void Main()
{
Person teacher = new Teacher("Nancy", "Davolio", 3);
var (firstName, lastName) = teacher; // Doesn't deconstruct Grade
Console.WriteLine($"{firstName}, {lastName}");// output: Nancy, Davolio
var (fName, lName, grade) = (Teacher)teacher;
Console.WriteLine($"{fName}, {lName}, {grade}");// output: Nancy, Davolio, 3
}
Generic constraints
There's no generic constraint that requires a type to be a record. Records satisfy either the class or struct constraint. To make a constraint on a specific hierarchy of record types, put the constraint on the base record as you would a base class. For more information, see Constraints on type parameters.
C# language specification
For more information, see the Classes section of the C# language specification.
For more information about features introduced in C# 9 and later, see the following feature proposal notes: