Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

C#11 – Records Demystified

5.00/5 (7 votes)
8 Feb 2023CPOL8 min read 13.8K   65  
Beginner’s tutorial on C#11 Records with examples
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” pattern and “Immutable Object” pattern. They are compiled into regular “classes” and “structs” in assembly/IL.

1. 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:

2. Records in C# - Summary

I am assuming that the reader is an intermediate-level C# programmer or higher 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. Actually, 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, you can live without records in your code and just use custom classes or structs to implement the “Value Object” pattern and “Immutable Object” pattern, as before. But, since that is a new language feature, so 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 can make structs records in C#10.
  • Are “records” immutable? In regular basic definition, as with regular classes and structs, records are mutable. 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, which is a one-liner constructor look-alike type definition for records, but 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 actually works with regular structs, too. When assigning structs, that keyword does “shallow clone” and mutates a small number of 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, and that is the reason 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.

C#
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

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

C#
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.

C#
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

In order to see what the compiler is really doing with the record, I used the decompiler tool dotPeek to decompile the assembly. It has the option to then create what they call “low-level-C#” from IL. So, what I did is C# source->assembly->IL->low-level-C#. Then I reduced a bit all the trash info that was in that file and removed method contents for brevity. The result is the brief outline of 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 records are all about.

C#
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.

C#
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.

C#
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, which is a one-liner constructor look-alike type definition for records, but 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:

C#
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

In order to see what the compiler is really doing with the record, I used the decompiler tool dotPeek to decompile the assembly. It has the option to then create what they call low-level-C# from IL. So, what I did is C# source->assembly->IL->low-level-C#. Then I reduced a bit all the trash info that was in that file and removed method contents for brevity. The result is the brief outline of 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 all about.

Here is what the above three Positional Syntax records generated code looks like:

C#
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 many times as you want, because is it 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.

C#
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:

C#
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 of manageable size, 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”.

The idea to integrate 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 he/she will stumble upon code where they are used. And the last reason, 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

History

  • 8th February, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)