C# 11 - Records Demystified

Abstract: This is a beginner’s tutorial on Records in C# with examples. C# Records are nothing more than C# language integrated support for the “Value Object” and “Immutable Object” patterns. They are compiled into regular “classes” and “structs” in assembly/IL.

Prerequisites

Please read up on the “Value Object” pattern and “Immutable Object” pattern since they are the motivation for introducing “records” into C# language. Here are some of my articles that are prerequisites for this tutorial:

Records in C# - Summary

I assume the reader is an intermediate-level C# programmer or upper and will go straight to the point, trying to demystify “Records”.

  • What are C# “records” similar to? They are similar to regular classes or structs in C# language. When compiled into IL, they are compiled into regular classes or structs in IL. Records exist only in the imagination of the C# compiler and are not a new concept to IL.
  • How are C# “records” different from regular classes or structs? Well, C# records are kind of “C# language integrated” support for the “Value Object” pattern and “Immutable Object” pattern. That means, for example, that the keyword “record” results in the automatic generation of several methods overloads (related to object equality) to give a class object “value semantics” ([5]).
  • What do C# “records” bring to the table if we already have classes and structs in C#? The answer is that you can live without records in your code and use custom classes or structs to implement the “Value Object” and “Immutable Object” patterns, as before. But, since that is a new language feature, it is fancy to start using it. Also, it is advertised as helping programmers to achieve the same functionality in fewer lines of code.
  • How do you define “records”? In the same place where you use “class” or “struct” keywords, you would use “record class” or “record struct”. The usage of just “record” is a synonym for “record class” since they first invented records for C#9 for classes, then they figured out they could make structs records in C#10.
  • Are “records” immutable? Records are mutable in regular basic definition, as with regular classes and structs. But the typical use case of records might require making them immutable, and you can make them immutable just as any class or struct.
  • Any other special syntax to mention? Yes, they invented a “Positional Syntax”, a one-liner constructor look-alike type definition for records. Still, in the background, a regular full-blown class or struct that implements their interpretation of the “Value (and sometime Immutable) Object” pattern is generated by the C# compiler.
  • What about the new contextual “with” keyword? That keyword works with regular structs, too. When assigning structs, that keyword does “shallow clone” and mutates several properties of your choice. The industry name for that is “Nondestructive mutation”.

Of course, there are technical details on how to implement “records’ in code, but I thought the concept of what we are achieving and what problem we are trying to solve with records needs to be addressed first.

3 Utility for finding object Addresses

We developed a small utility that will give us the address of the objects in question, so by comparing addresses, it will be easily seen if we are talking about the same or different objects. The only problem is that our address-finding-utility has a limitation, that is, it works ONLY for objects on the heap that do not contain other objects on the heap (references). Therefore, we are forced to use only primitive values in our objects, which is why I needed to avoid using C# “string” and am using only “char” types. The code is in the attached projects.

4 Record Class Mutable - Example

Here we will give an example of a Record Class Mutable object and demo its behavior in typical situations, like mutation, assignment, and equality comparison.

public record class CarRecordClassMutable {
    public CarRecordClassMutable(Char ? brand, Char ? model, int ? year) {
        Brand = brand;
        Model = model;
        Year = year;
    }
    public Char ? Brand {
        get;
        set;
    }
    public Char ? Model {
        get;
        set;
    }
    public int ? Year {
        get;
        set;
    }
    public override string ToString() {
        return $ "Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}
//=============================================
//===Sample code===============================
//class based objects
Console.WriteLine("-----");
Console.WriteLine("Mutation of mutable record class object");
CarRecordClassMutable car1 = new CarRecordClassMutable('T', 'C', 2022);
Console.WriteLine($ "Before mutation: car1={car1}");
car1.Model = 'A';
Console.WriteLine($ "After  mutation: car1={car1}");
Console.WriteLine();
//--assigning class based objects
Console.WriteLine("-----");
Console.WriteLine("Assignment of mutable record class object");
Console.WriteLine("From addresses you can see that assignment created ");
Console.WriteLine("two references pointing to the same object on heap ");
CarRecordClassMutable car3 = new CarRecordClassMutable('T', 'C', 1991);
CarRecordClassMutable car4 = car3;
Tuple < string ? , string ? > addresses1 = Util.GetMemoryAddressOfClass(car3, car4);
Console.WriteLine($ "Address car3={addresses1.Item1}, Address car4={addresses1.Item2}");
Console.WriteLine($ "Before mutation: car3={car3}");
Console.WriteLine($ "Before mutation: car4={car4}");
car4.Model = 'Y';
Console.WriteLine($ "After  mutation: car3={car3}");
Console.WriteLine($ "After  mutation: car4={car4}");
Console.WriteLine();
//--equality of class based objects
Console.WriteLine("Equality of record class object");
CarRecordClassMutable car31 = new CarRecordClassMutable('T', 'C', 1991);
CarRecordClassMutable car41 = new CarRecordClassMutable('T', 'C', 1991);
Console.WriteLine($ "record class object, car31={car31}");
Console.WriteLine($ "record class object, car41={car41}");
bool equal3141 = car31 == car41;
Console.WriteLine($ "car31 == car41:{equal3141}");
Console.WriteLine();
//=============================================
//===Result of execution=======================
/*
-----
Mutation of mutable record class object
Before mutation: car1=Brand:T, Model:C, Year:2022
After  mutation: car1=Brand:T, Model:A, Year:2022

-----
Assignment of mutable record class object
From addresses you can see that assignment created
two references pointing to the same object on heap
Address car3=0x28B76160478, Address car4=0x28B76160478
Before mutation: car3=Brand:T, Model:C, Year:1991
Before mutation: car4=Brand:T, Model:C, Year:1991
After  mutation: car3=Brand:T, Model:Y, Year:1991
After  mutation: car4=Brand:T, Model:Y, Year:1991

Equality of record class object
record class object, car31=Brand:T, Model:C, Year:1991
record class object, car41=Brand:T, Model:C, Year:1991
car31 == car41:True
*/

4.1 Record Class Mutable- Decompiled

I used the decompiler tool dotPeek to decompile the assembly to see what the compiler is doing with the record. It has the option to create what they call “low-level-C#” from IL. So, what I did is “C# source”->assembly->IL->“low-level-C#”. Then I reduced all the trash info in that file and removed method contents for brevity. The result outlines the equivalent C# code generated by the C# compiler. That gives us a pretty good idea of what is happening behind the scenes and what the records are about.

public class CarRecordClassMutable: IEquatable < CarRecordClassMutable > {
    private char ? \u003CBrand\u003Ek__BackingField;
    private char ? \u003CModel\u003Ek__BackingField;
    private int ? \u003CYear\u003Ek__BackingField;
    protected virtual Type EqualityContract {
        /* removed for brevity*/ }
    public CarRecordClassMutable(char ? brand, char ? model, int ? year) {
        /* removed for brevity*/ }
    public char ? Brand {
        /* removed for brevity*/ }
    public char ? Model {
        /* removed for brevity*/ }
    public int ? Year {
        /* removed for brevity*/ }
    public override string ToString() {
        /* removed for brevity*/ }
    protected virtual bool PrintMembers(StringBuilder builder) {
        /* removed for brevity*/ }
    public static bool op_Inequality(CarRecordClassMutable left, CarRecordClassMutable right) {
        /* removed for brevity*/ }
    public static bool op_Equality(CarRecordClassMutable left, CarRecordClassMutable right) {
        /* removed for brevity*/ }
    public override int GetHashCode() {
        /* removed for brevity*/ }
    public override bool Equals(object obj) {
        /* removed for brevity*/ }
    public virtual bool Equals(CarRecordClassMutable other) {
        /* removed for brevity*/ }
    public virtual CarRecordClassMutable \u003CClone\u003E\u0024() {
        /* removed for brevity*/ }
    protected CarRecordClassMutable(CarRecordClassMutable original) {
        /* removed for brevity*/ }
}

5 Record Struct Mutable - Example

Here we will give an example of a Record Struct Mutable object and demo its behavior in typical situations, like mutation, assignment, and equality comparison.

public record struct CarRecordStructMutable {
    public CarRecordStructMutable(Char ? brand, Char ? model, int ? year) {
        Brand = brand;
        Model = model;
        Year = year;
    }
    public Char ? Brand {
        get;
        set;
    }
    public Char ? Model {
        get;
        set;
    }
    public int ? Year {
        get;
        set;
    }
    public override string ToString() {
        return $ "Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}
//=============================================
//===Sample code===============================
//struct based objects
Console.WriteLine("-----");
Console.WriteLine("Mutation of mutable record struct object");
CarRecordStructMutable car5 = new CarRecordStructMutable('T', 'C', 2022);
Console.WriteLine($ "Before mutation: car5={car5}");
car5.Model = 'Y';
Console.WriteLine($ "After  mutation: car5={car5}");
Console.WriteLine();
//--assigning struct based objects
Console.WriteLine("-----");
Console.WriteLine("Assignment of mutable record struct object");
Console.WriteLine("From addresses you can see that assignment created ");
Console.WriteLine("two different objects on the stack ");
CarRecordStructMutable car7 = new CarRecordStructMutable('T', 'C', 1991);
CarRecordStructMutable car8 = car7;
string ? address7 = Util.GetMemoryAddressOfStruct(ref car7);
string ? address8 = Util.GetMemoryAddressOfStruct(ref car8);
Console.WriteLine($ "Address car7={address7}, Address car8={address8}");
Console.WriteLine($ "Before mutation: car7={car7}");
Console.WriteLine($ "Before mutation: car8={car8}");
car8.Model = 'M';
Console.WriteLine($ "After  mutation: car7={car7}");
Console.WriteLine($ "After  mutation: car8={car8}");
Console.WriteLine();
//--equality of struct based objects
Console.WriteLine("Equality of record struct object");
CarRecordStructMutable car71 = new CarRecordStructMutable('T', 'C', 1991);
CarRecordStructMutable car81 = new CarRecordStructMutable('T', 'C', 1991);
Console.WriteLine($ "record struct object, car71={car71}");
Console.WriteLine($ "record struct object, car81={car81}");
bool equal7181 = car71 == car81;
Console.WriteLine($ "car71 == car81:{equal7181}");
Console.WriteLine();
//=============================================
//===Result of execution=======================
/*
-----
Mutation of mutable record struct object
Before mutation: car5=Brand:T, Model:C, Year:2022
After  mutation: car5=Brand:T, Model:Y, Year:2022

-----
Assignment of mutable record struct object
From addresses you can see that assignment created
two different objects on the stack
Address car7=0xB46117E4C8, Address car8=0xB46117E4B8
Before mutation: car7=Brand:T, Model:C, Year:1991
Before mutation: car8=Brand:T, Model:C, Year:1991
After  mutation: car7=Brand:T, Model:C, Year:1991
After  mutation: car8=Brand:T, Model:M, Year:1991

Equality of record struct object
record struct object, car71=Brand:T, Model:C, Year:1991
record struct object, car81=Brand:T, Model:C, Year:1991
car71 == car81:True
*/

5.1 Record Struct Mutable- Decompiled

I used the decompiler tool dotPeek to decompile the assembly to see what the compiler is doing with the record. It has the option to create what they call “low-level-C#” from IL. So, what I did is “C# source”->assembly->IL->“low-level-C#”. Then I reduced all the trash info in that file and removed method contents for brevity. The result outlines the equivalent C# code generated by the C# compiler. That gives us a pretty good idea of what is happening behind the scenes and what the records are about.

public struct CarRecordStructMutable: IEquatable < CarRecordStructMutable > {
    private char ? \u003CBrand\u003Ek__BackingField;
    private char ? \u003CModel\u003Ek__BackingField;
    private int ? \u003CYear\u003Ek__BackingField;
    public CarRecordStructMutable(char ? brand, char ? model, int ? year) {
        /* removed for brevity*/ }
    public char ? Brand {
        /* removed for brevity*/ }
    public char ? Model {
        /* removed for brevity*/ }
    public int ? Year {
        /* removed for brevity*/ }
    public override string ToString() {
        /* removed for brevity*/ }
    private readonly bool PrintMembers(StringBuilder builder) {
        /* removed for brevity*/ }
    public static bool op_Inequality(CarRecordStructMutable left, CarRecordStructMutable right) {
        /* removed for brevity*/ }
    public static bool op_Equality(CarRecordStructMutable left, CarRecordStructMutable right) {
        /* removed for brevity*/ }
    public override readonly int GetHashCode() {
        /* removed for brevity*/ }
    public override readonly bool Equals(object obj) {
        /* removed for brevity*/ }
    public readonly bool Equals(CarRecordStructMutable other) {
        /* removed for brevity*/ }
}

6 Record Class Immutable - Example

Here we will give an example of a Record Class Immutable object and demo its behavior in typical situations, like mutation, assignment, and equality comparison.

public record class CarRecordClassImmutable {
    public CarRecordClassImmutable(Char ? brand, Char ? model, int ? year) {
        Brand = brand;
        Model = model;
        Year = year;
    }
    public Char ? Brand {
        get;
        init;
    }
    public Char ? Model {
        get;
        init;
    }
    public int ? Year {
        get;
        init;
    }
    public override string ToString() {
        return $ "Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}
//=============================================
//===Sample code===============================
//class based objects
Console.WriteLine("-----");
Console.WriteLine("Mutation of immutable record class object");
CarRecordClassImmutable car1 = new CarRecordClassImmutable('T', 'C', 2022);
//next line will not compile, since is readonly property
//car1.Model = 'A';
Console.WriteLine();
//--assigning class based objects
Console.WriteLine("-----");
Console.WriteLine("Assignment of immutable record class object");
Console.WriteLine("From addresses you can see that assignment created ");
Console.WriteLine("two references pointing to the same object on heap ");
CarRecordClassImmutable car3 = new CarRecordClassImmutable('T', 'C', 1991);
CarRecordClassImmutable car4 = car3;
Tuple < string ? , string ? > addresses1 = Util.GetMemoryAddressOfClass(car3, car4);
Console.WriteLine($ "Address car3={addresses1.Item1}, Address car4={addresses1.Item2}");
Console.WriteLine();
//--equality of class based objects
Console.WriteLine("Equality of record class object");
CarRecordClassImmutable car31 = new CarRecordClassImmutable('T', 'C', 1991);
CarRecordClassImmutable car41 = new CarRecordClassImmutable('T', 'C', 1991);
Console.WriteLine($ "record class object, car31={car31}");
Console.WriteLine($ "record class object, car41={car41}");
bool equal3141 = car31 == car41;
Console.WriteLine($ "car31 == car41:{equal3141}");
Console.WriteLine();
//=============================================
//===Result of execution=======================
/*
-----
Mutation of immutable record class object

-----
Assignment of immutable record class object
From addresses you can see that assignment created
two references pointing to the same object on heap
Address car3=0x1E442D5F948, Address car4=0x1E442D5F948

Equality of record class object
record class object, car31=Brand:T, Model:C, Year:1991
record class object, car41=Brand:T, Model:C, Year:1991
car31 == car41:True
*/

7 Record Struct Immutable - Example

Here we will give an example of a Record Struct Immutable object and demo its behavior in typical situations, like mutation, assignment, and equality comparison.

public readonly record struct CarRecordStructImmutable {
    public CarRecordStructImmutable(Char ? brand, Char ? model, int ? year) {
        Brand = brand;
        Model = model;
        Year = year;
    }
    public Char ? Brand {
        get;
        init;
    }
    public Char ? Model {
        get;
        init;
    }
    public int ? Year {
        get;
        init;
    }
    public override string ToString() {
        return $ "Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}
//=============================================
//===Sample code===============================
//struct based objects
Console.WriteLine("-----");
Console.WriteLine("Mutation of immutable record struct object");
CarRecordStructImmutable car5 = new CarRecordStructImmutable('T', 'C', 2022);
//next line will not compile, since is readonly property
//car5.Model = 'Y';
Console.WriteLine();
//--assigning struct based objects
Console.WriteLine("-----");
Console.WriteLine("Assignment of immutable record struct object");
Console.WriteLine("From addresses you can see that assignment created ");
Console.WriteLine("two different objects on the stack ");
CarRecordStructImmutable car7 = new CarRecordStructImmutable('T', 'C', 1991);
CarRecordStructImmutable car8 = car7;
string ? address7 = Util.GetMemoryAddressOfStruct(ref car7);
string ? address8 = Util.GetMemoryAddressOfStruct(ref car8);
Console.WriteLine($ "Address car7={address7}, Address car8={address8}");
Console.WriteLine();
//--equality of struct based objects
Console.WriteLine("Equality of record struct object");
CarRecordStructImmutable car71 = new CarRecordStructImmutable('T', 'C', 1991);
CarRecordStructImmutable car81 = new CarRecordStructImmutable('T', 'C', 1991);
Console.WriteLine($ "record struct object, car71={car71}");
Console.WriteLine($ "record struct object, car81={car81}");
bool equal7181 = car71 == car81;
Console.WriteLine($ "car71 == car81:{equal7181}");
Console.WriteLine();
//=============================================
//===Result of execution=======================
/*
-----
Mutation of immutable record struct object

-----
Assignment of immutable record struct object
From addresses you can see that assignment created
two different objects on the stack
Address car7=0x8918B7E3B8, Address car8=0x8918B7E3A8

Equality of record struct object
record struct object, car71=Brand:T, Model:C, Year:1991
record struct object, car81=Brand:T, Model:C, Year:1991
car71 == car81:True
*/

8 “Positional Syntax” for defining Records

They invented a “Positional Syntax”, a one-liner constructor look-alike type definition for records. Still, in the background, a regular full-blown class or struct that implements their interpretation of the “Value (and sometime Immutable) Object” pattern is generated by the C# compiler. The code generated is similar to the examples above, just an extra method, “Decontruct()” is added. Here is what this short notation looks like:

public record struct CarRecordStructMutable(Char ? Brand, Char ? Model, int ? Year);
public readonly record struct CarRecordStructImmutable(Char ? Brand, Char ? Model, int ? Year);
public record class CarRecordClassImmutable(Char ? Brand, Char ? Model, int ? Year);
//the following line will not compile, readonly can not be applied to class
//public readonly record class CarRecordClassImmutable2(Char? Brand, Char? Model, int? Year);   

8.1 “Positional Syntax” for defining Records-Decompiled

I used the decompiler tool dotPeek to decompile the assembly to see what the compiler is doing with the record. It has the option to create what they call “low-level-C#” from IL. So, what I did is “C# source”->assembly->IL->“low-level-C#”. Then I reduced all the trash info in that file and removed method contents for brevity. The result outlines the equivalent C# code generated by the C# compiler. That gives us a pretty good idea of what is happening behind the scenes and what the records are about.

Here is what the above 3 “Positional Syntax” records generated code looks like.

public struct CarRecordStructMutable2: IEquatable < PositionalSyntax.CarRecordStructMutable2 > {
    private char ? \u003CBrand\u003Ek__BackingField;
    private char ? \u003CModel\u003Ek__BackingField;
    private int ? \u003CYear\u003Ek__BackingField;
    public CarRecordStructMutable2(char ? Brand, char ? Model, int ? Year) {
        /* removed for brevity*/ }
    public char ? Brand {
        readonly get {
            return this.\u003CBrand\u003Ek__BackingField;
        }
        set {
            this.\u003CBrand\u003Ek__BackingField = value;
        }
    }
    public char ? Model {
        readonly get {
            return this.\u003CModel\u003Ek__BackingField;
        }
        set {
            this.\u003CModel\u003Ek__BackingField = value;
        }
    }
    public int ? Year {
        readonly get {
            return this.\u003CYear\u003Ek__BackingField;
        }
        set {
            this.\u003CYear\u003Ek__BackingField = value;
        }
    }
    public override readonly string ToString() {
        /* removed for brevity*/ }
    private readonly bool PrintMembers(StringBuilder builder) {
        /* removed for brevity*/ }
    public static bool op_Inequality(PositionalSyntax.CarRecordStructMutable2 left, PositionalSyntax.CarRecordStructMutable2 right) {
        /* removed for brevity*/ }
    public static bool op_Equality(PositionalSyntax.CarRecordStructMutable2 left, PositionalSyntax.CarRecordStructMutable2 right) {
        /* removed for brevity*/ }
    public override readonly int GetHashCode() {
        /* removed for brevity*/ }
    public override readonly bool Equals(object obj) {
        /* removed for brevity*/ }
    public readonly bool Equals(PositionalSyntax.CarRecordStructMutable2 other) {
        /* removed for brevity*/ }
    public readonly void Deconstruct(out char ? Brand, out char ? Model, out int ? Year) {
        /* removed for brevity*/ }
}
//===========================================================================
public readonly struct CarRecordStructImmutable2: IEquatable < PositionalSyntax.CarRecordStructImmutable2 > {
    private readonly char ? \u003CBrand\u003Ek__BackingField;
    private readonly char ? \u003CModel\u003Ek__BackingField;
    private readonly int ? \u003CYear\u003Ek__BackingField;
    public CarRecordStructImmutable2(char ? Brand, char ? Model, int ? Year) {
        /* removed for brevity*/ }
    public char ? Brand {
        get {
            return this.\u003CBrand\u003Ek__BackingField;
        }
        init {
            this.\u003CBrand\u003Ek__BackingField = value;
        }
    }
    public char ? Model {
        get {
            return this.\u003CModel\u003Ek__BackingField;
        }
        init {
            this.\u003CModel\u003Ek__BackingField = value;
        }
    }
    public int ? Year {
        get {
            return this.\u003CYear\u003Ek__BackingField;
        }
        init {
            this.\u003CYear\u003Ek__BackingField = value;
        }
    }
    public override string ToString() {
        /* removed for brevity*/ }
    private bool PrintMembers(StringBuilder builder) {
        /* removed for brevity*/ }
    public static bool op_Inequality(PositionalSyntax.CarRecordStructImmutable2 left, PositionalSyntax.CarRecordStructImmutable2 right) {
        /* removed for brevity*/ }
    public static bool op_Equality(PositionalSyntax.CarRecordStructImmutable2 left, PositionalSyntax.CarRecordStructImmutable2 right) {
        /* removed for brevity*/ }
    public override int GetHashCode() {
        /* removed for brevity*/ }
    public override bool Equals(object obj) {
        /* removed for brevity*/ }
    public bool Equals(PositionalSyntax.CarRecordStructImmutable2 other) {
        /* removed for brevity*/ }
    public void Deconstruct(out char ? Brand, out char ? Model, out int ? Year) {
        /* removed for brevity*/ }
}
//===========================================================================
public class CarRecordClassImmutable2: IEquatable < PositionalSyntax.CarRecordClassImmutable2 > {
    private readonly char ? \u003CBrand\u003Ek__BackingField;
    private readonly char ? \u003CModel\u003Ek__BackingField;
    private readonly int ? \u003CYear\u003Ek__BackingField;
    public CarRecordClassImmutable2(char ? Brand, char ? Model, int ? Year) {
        /* removed for brevity*/ }
    protected virtual Type EqualityContract {
        /* removed for brevity*/ }
    public char ? Brand {
        get {
            return this.\u003CBrand\u003Ek__BackingField;
        }
        init {
            this.\u003CBrand\u003Ek__BackingField = value;
        }
    }
    public char ? Model {
        get {
            return this.\u003CModel\u003Ek__BackingField;
        }
        init {
            this.\u003CModel\u003Ek__BackingField = value;
        }
    }
    public int ? Year {
        get {
            return this.\u003CYear\u003Ek__BackingField;
        }
        init {
            this.\u003CYear\u003Ek__BackingField = value;
        }
    }
    public override string ToString() {
        /* removed for brevity*/ }
    protected virtual bool PrintMembers(StringBuilder builder) {
        /* removed for brevity*/ }
    public static bool op_Inequality(PositionalSyntax.CarRecordClassImmutable2 left, PositionalSyntax.CarRecordClassImmutable2 right) {
        /* removed for brevity*/ }
    public static bool op_Equality(PositionalSyntax.CarRecordClassImmutable2 left, PositionalSyntax.CarRecordClassImmutable2 right) {
        /* removed for brevity*/ }
    public override int GetHashCode() {
        /* removed for brevity*/ }
    public override bool Equals(object obj) {
        /* removed for brevity*/ }
    public virtual bool Equals(PositionalSyntax.CarRecordClassImmutable2 other) {
        /* removed for brevity*/ }
    public virtual PositionalSyntax.CarRecordClassImmutable2 \u003CClone\u003E\u0024() {
        /* removed for brevity*/ }
    protected CarRecordClassImmutable2(PositionalSyntax.CarRecordClassImmutable2 original) {
        /* removed for brevity*/ }
    public void Deconstruct(out char ? Brand, out char ? Model, out int ? Year) {
        /* removed for brevity*/ }
}

9 Nondestructive mutation

If you want to reuse an Immutable record, you are free to reference it as often as you want because it is guaranteed not to change. But what if you want to reuse some of the data of an Immutable record but modify it a bit? That is why they invented “Nondestructive Mutation”. In C# language, now you can use the “with” keyword to do it. Typically, you would want to preserve most of the state of an Immutable record but change just some properties.

public readonly record struct CarRecordStructImmutable2(Char? Brand, Char? Model, int? Year);
public record class CarRecordClassImmutable2(Char? Brand, Char? Model, int? Year);

//=============================================
//===Sample code===============================
//struct based objects
Console.WriteLine("-----");
Console.WriteLine("Nondestructive Mutation of immutable record struct object");
CarRecordStructImmutable2 car7 = new CarRecordStructImmutable2('T', 'C', 1991);
CarRecordStructImmutable2 car8 = car7 with { Brand = 'A' };

string? address1 = Util.GetMemoryAddressOfStruct(ref car7);
string? address2 = Util.GetMemoryAddressOfStruct(ref car8);
Console.WriteLine($"Address car7={address1}, Address car8={address2}");

Console.WriteLine($"State: car7={car7}");
Console.WriteLine($"State: car8={car8}");
Console.WriteLine();

//class based objects
Console.WriteLine("-----");
Console.WriteLine("Nondestructive Mutation of immutable record class object");
CarRecordClassImmutable2 car1 = new CarRecordClassImmutable2('T', 'C', 1991);
CarRecordClassImmutable2 car2 = car1 with { Brand = 'p' };

Tuple<string?, string?> addresses2 = Util.GetMemoryAddressOfClass(car1, car2);
Console.WriteLine($"Address car1={addresses2.Item1}, Address car2={addresses2.Item2}");

Console.WriteLine($"State: car1={car1}");
Console.WriteLine($"State: car2={car2}");
Console.WriteLine();

Console.ReadLine();
//=============================================
//===Result of execution=======================
/*
-----
Nondestructive Mutation of immutable record struct object
Address car7=0xBE9497EB60, Address car8=0xBE9497EB50
State: car7=CarRecordStructImmutable2 { Brand = T, Model = C, Year = 1991 }
State: car8=CarRecordStructImmutable2 { Brand = A, Model = C, Year = 1991 }

-----
Nondestructive Mutation of immutable record class object
Address car1=0x27B92562AE8, Address car2=0x27B92562B08
State: car1=CarRecordClassImmutable2 { Brand = T, Model = C, Year = 1991 }
State: car2=CarRecordClassImmutable2 { Brand = p, Model = C, Year = 1991 }
*/

10 Deconstructing Records

As can be seen above, Records defined with “Positional Syntax” also have an automatically generated “Deconstruct()” method. It behaves as one would expect, similar to many other cases of “deconstruction” in C#.
Records that are not defined with “Positional Syntax” but with “record class” or “record struct” prefixes do not have the “Deconstruct()” method unless the programmer explicitly defines one.
Here are some examples:

public record struct CarRecordStructMutable2(Char ? Brand, Char ? Model, int ? Year);
public readonly record struct CarRecordStructImmutable2(Char ? Brand, Char ? Model, int ? Year);
public record class CarRecordClassImmutable2(Char ? Brand, Char ? Model, int ? Year);
//=============================================
//===Sample code===============================
Console.WriteLine("-----");
CarRecordStructMutable2 car6 = new CarRecordStructMutable2('A', '1', 2000);
CarRecordStructImmutable2 car7 = new CarRecordStructImmutable2('B', '2', 2001);
CarRecordClassImmutable2 car8 = new CarRecordClassImmutable2('C', '3', 2002);
// deconstucting CarRecordStructMutable2 car6
var (a1, b1, c1) = car6;
Console.WriteLine($ "State: car6={car6}");
Console.WriteLine($ "a1:{a1}, b1:{b1}, c1:{c1}");
// deconstucting CarRecordStructImmutable2 car7
car7.Deconstruct(out char ? a2, out char ? b2, out int ? c2);
Console.WriteLine($ "State: car7={car7}");
Console.WriteLine($ "a2:{a2}, b2:{b2}, c2:{c2}");
// deconstucting CarRecordClassImmutable2 car8
(char ? a3, char ? b3, int ? c3) = car8;
Console.WriteLine($ "State: car8={car8}");
Console.WriteLine($ "a3:{a3}, b3:{b3}, c3:{c3}");
//=============================================
//===Result of execution=======================
/*
-----
State: car6=CarRecordStructMutable2 { Brand = A, Model = 1, Year = 2000 }
a1:A, b1:1, c1:2000
State: car7=CarRecordStructImmutable2 { Brand = B, Model = 2, Year = 2001 }
a2:B, b2:2, c2:2001
State: car8=CarRecordClassImmutable2 { Brand = C, Model = 3, Year = 2002 }
a3:C, b3:3, c3:2002
*/

11 Topics related to Records not covered

This is just a basic introduction to Records, and to keep this article manageable, some topics are not covered. They are:

  • Record inheritance

12 Conclusion

C# Records are an interesting new feature of C#11, but all the concepts behind them have already been seen. C# Records are nothing more than C# language integrated support for the “Value Object” pattern ([8]) and the “Immutable Object” pattern ([7]). Records exist only in the imagination of the C# compiler and are compiled into regular “classes” and “structs”.

Integrating into C# language support for certain patterns is not new. For example, the “Observer pattern” ([6]) is integrated into C# via the usage of the Event mechanism.

Like any other new feature, their usage will propagate over time, and every modern C# programmer needs to know them well, if nothing else, because, over time, they will stumble upon code where they are used. And the last reason is that you need to learn and use Records, or otherwise you will be considered a “dinosaur C# programmer” that doesn’t know modern C#.

13 References

  1. Andrew Troelsen, Phil Japikse: Pro C# 10 with .NET 6, 11th Edition, 2022
  2. Joseph Albahari: C# 10 in a Nutshell, 2022
  3. https://en.wikipedia.org/wiki/Value_object
  4. https://en.wikipedia.org/wiki/Data_transfer_object
  5. https://en.wikipedia.org/wiki/Value_semantics
  6. https://www.codeproject.com/Articles/5326833/Observer-Pattern-in-Csharp
  7. https://www.codeproject.com/Articles/5353999/Csharp11-Immutable-Object-Pattern
  8. https://www.codeproject.com/Articles/5354124/Csharp-Value-Object-Pattern-Data-Transfer-Object-P


Similar Articles