Deep Dive Into Records In C# 9

C# does not quite support immutability out-of-the-box -- it didn't provide native support for creating such immutable objects until C# 9. Having said so, you could still create immutable types with C# 8 or earlier, but that requires quite a lot of boiler-plate code. When one considers the merits of the immutable objects, quite often you tend to take the long tedious path of creating the immutable objects in C#, despite not exactly liking what you are doing.
 
All that is going to change with the introduction of records in C# 9. And what's more, the records support a lot of other functionalities, all of which might have taken you a lot more lines of code. Let us first introduce the record syntax and then evaluate the benefits. We will also check what it would have taken for a C# 8 developer to write a similiar type.
  1. public record User(string UserName,int Id); 
That one line of code creates for us an entire class that is immutable - with readonly properties. The syntax above also introduces us to another feature - Primary constructor, proposal of which could be seen here. But at this point of time, I am not sure if the primary constructors would find a way outside records.
 
The above line would roughly translate to
  1. // Partial generated code  
  2. public class User  
  3. {  
  4.     public string UserName{get;init;}  
  5.     public int Id{get;init;}  
  6.   
  7.     public User(string userName,int id)  
  8.     {  
  9.         (UserName,Id) = (userName,id);  
  10.     }  

 At this point, do note that it is not a must that you need to stick to Primary Constructor. You could declare the record with the same syntax as you would do a class declaration. For example,
  1. public record Person  
  2. {  
  3.     public string UserName { get; init; }  
  4.     public int Id { get; init; }  

init accessor could be seen as a variant of set, which works only with object initializers. Once the object has been initialized, the property becomes effectively a readonly property.
 
If you had noticied, I had used the word roughly earlier while describing the generated code. This is because the compiler does a lot more than providing immutable types.
 

Structural Equality

 
Unlike a class, records follows structural equality instead of referential equality. Structural equality ensures that two records are considered equal if all the properties are equal.
 
The compiler generates for us all the boiler plate code which previously, one would have to take pains to write to attain structural equality by overriding Equals and GetHashCode methods. This means that our previous one-liner declaration of record would be translated by the compiler as the following.
  1. public class User : IEquatable<User>  
  2. {  
  3.     public string UserName{get;init;}  
  4.     public int Id{get;init;}  
  5.   
  6.     public User(string userName,int id)  
  7.     {  
  8.         (UserName,Id) = (userName,id);  
  9.     }  
  10.   
  11.     public override int GetHashCode()  
  12.     {  
  13.         return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(UserName)) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(Id);  
  14.     }  
  15.   
  16.     public override bool Equals(object? obj)  
  17.     {  
  18.         return Equals(obj as User);  
  19.     }  
  20.   
  21.     [System.Runtime.CompilerServices.NullableContext(2)]  
  22.     public static bool operator !=(User? r1, User? r2)  
  23.     {  
  24.         return !(r1 == r2);  
  25.     }  
  26.   
  27.     [System.Runtime.CompilerServices.NullableContext(2)]  
  28.     public static bool operator ==(User? r1, User? r2)  
  29.     {  
  30.         return (object)r1 == r2 || (r1?.Equals(r2) ?? false);  
  31.     }  

Notice that the compiler has overridden the == and != operators for you as well.
 
We could verify the functionality as following.
  1. var user1 = new User("Anu Viswan", 1);  
  2. var user2 = new User("Anu Viswan", 1);  
  3. var user3 = new User("Manu Viswan", 1);  
  4.   
  5.  // Structural Equality  
  6. Console.WriteLine($"user1==user2 : {user1 == user2}");  
  7. Console.WriteLine($"user1==user3 : {user1 == user3}");  
  8.   
  9. // Output  
  10. // user1==user2 : True  
  11. // user1==user3 : False 
We have already seen how the records have made our code much simpler by removing a lot of boilerplate code, reducing complexity of your codebase. But we aren't done yet.
 

Deconstructor

 
The compiler generates the deconstruct code for us behind the scenes. This allows us to convert a record to a tuple easily. For example,
  1. var user = new User("Anu Viswan", 1);  
  2. var (userName, id) = user; 

ToString()

 
The compiler also generates a useful ToString() override for the records, which produces a JSON representation of tuple.
  1. var user = new User("Anu Viswan", 1);  
  2. Console.WriteLine(user.ToString());  
  3.   
  4. // Output  
  5. // User { UserName = Anu Viswan, Id = 1 } 
 At this point, let us get an complete override of all the code that is being generated by the compiler. For the sake of brevity of code, i have hidden the method definitions, but it would still give you a picture of how many lines of code one could skip by using the records.
  1. public class User: IEquatable < User > {  
  2.     protected virtual Type EqualityContract {  
  3.         /* Code Hidden */ }  
  4.     public string UserName {  
  5.         get;  
  6.         init;  
  7.     }  
  8.     public int Id {  
  9.         get;  
  10.         init;  
  11.     }  
  12.     public User(string UserName, int Id) {  
  13.         /* Code Hidden */ }  
  14.     public override string ToString() {  
  15.         /* Code Hidden */ }  
  16.     protected virtual bool PrintMembers(StringBuilder builder) {  
  17.         /* Code Hidden */ }  
  18.     [System.Runtime.CompilerServices.NullableContext(2)]  
  19.     public static bool operator != (User ? r1, User ? r2) {  
  20.         /* Code Hidden */ }  
  21.     [System.Runtime.CompilerServices.NullableContext(2)]  
  22.     public static bool operator == (User ? r1, User ? r2) {  
  23.         /* Code Hidden */ }  
  24.     public override int GetHashCode() {  
  25.         /* Code Hidden */ }  
  26.     public override bool Equals(object ? obj) {  
  27.         /* Code Hidden */ }  
  28.     public virtual bool Equals(User ? other) {  
  29.         /* Code Hidden */ }  
  30.     public virtual User < Clone > $() {  
  31.         /* Code Hidden */ }  
  32.     protected User(User original) {  
  33.         /* Code Hidden */ }  
  34.     public void Deconstruct(out string UserName, out int Id) {  
  35.         /* Code Hidden */ }  
  36. }   
In addition to above discussions, there are some traits of records that one needs to be aware of.
 

Inheritence

 
Being an object oriented  language, it is natural that the records support inheritence, but with a caveat. You could inherit a record from only a record. Consider the following code.
  1. public record User(string UserName, int Id);  
  2.   
  3. public record Customer(string UserName,int Id,string Location):User(UserName, Id); 
 While the above code is valid, the following is not valid in C#.
  1. public record Person{}  
  2.   
  3. // Following would not compiler - A class cannot inherit from a Record  
  4. public class Employee: Person {}  
  5.   
  6. // Same is case here, invalid - A record can inherit only from another record  
  7. public class Foo { }  
  8. public record Bar : Foo {} 
Inheritence rules could be summarized as
  • A record can inherit only from another record
  • A class cannot inherit from a record
There is another important point worth noticing. Consider the following inheritence.
  1. public record User(string UserName, int Id);  
  2.   
  3. public record Customer(string UserName,int Id,string Location):User(UserName, Id);  
  4.   
  5. public record Employee(string DisplayName,int Id,string Location):User(DisplayName, Id); 
In the above scenario, the customer would have 3 properties - UserName,Id and Location. Two of them are being inherited from the base record, while the third one, Location, is part of the Customer record.
 
The scenario is slightly different with the Employee record. In this case, Employee has 4 Properties - DisplayName, Location, UserName, and Id. White it inherits UserName and Id from User, it creates two new properties called DisplayName and Location. For example,
  1. var user = new User("Anu Viswan",1);  
  2. var customer = new Customer("Anu Viswan", 1, "India");  
  3. var employee = new Employee("Anu Viswan", 1, "India");  
  4.   
  5. Console.WriteLine(user.ToString());  
  6. Console.WriteLine(customer.ToString());  
  7. Console.WriteLine(employee.ToString());  
  8.   
  9. // Output  
  10. User { UserName = Anu Viswan, Id = 1 }  
  11. Customer { UserName = Anu Viswan, Id = 1, Location = India }  
  12. Employee { UserName = Anu Viswan, Id = 1, DisplayName = Anu Viswan, Location = India } 
 Another significant point one needs to be aware of with regard to inheritence of records is how the Derieved record is deconstructed. The generated deconstructor of the derieved record matches the parameter paritiy of its own constructor. It does not deconstruct the parent record. For example, the deconstruct of the Employee record would be generated as following.
  1. public void Deconstruct(out string DisplayName, out int Id, out string Location)  
  2. {  
  3.     DisplayName = this.DisplayName;  
  4.     Id = base.Id;  
  5.     Location = this.Location;  

With Keyword


Records introduces a new keyword to C# with, which would help us work with immutability. Consider the scenario when you want to create an instance of a record, which vary from another instance from only a couple of property. In this scenario, you would like to clone the original instance and change the property which differs. But, remember - records are immutable. You cannot change it once initialized. This is where with keyword steps in. The with keyword clones the original property and changes the required properties before the initalization is complete. Let us see with in action now before discussing further.
  1. var anotherUser = user with  
  2. {  
  3.     UserName = "Manu Viswan"  
  4. };  
  5.   
  6. // Output  
  7. User { UserName = Manu Viswan, Id = 1 } 
What happens behind the scenes is that the compiler calls a hidden Clone method, which internally uses the copy constructor to clone the object, and then it changes the properties.
 
The important point to remember with with uses non-desctructive mutation to create a new instance of the record cloning the original instance (with some properties different as mentioned in the with clause), ensuring immutability.
 
The with keywords works well with inheritence as well. Each type will call its hidden Clone() method, which in turn starts a chained call of copy constructors that extends to the parent's copy constructors.
 

Custom Cloning

 
You could in fact customize the behavior of the copy constructor by writing your own copy constructor. For example, in the previous example, if you would like to reset the Id to -1 when creating a copy using the with keyword, you could writing your custom copy constructor as the following.
  1. public record User(string UserName, int Id)  
  2. {  
  3.     public User(User original) => (UserName, Id) = (original.UserName, -1);  

Now exucuting our previous code, would generate the following output.
  1. User { UserName = Manu Viswan, Id = -1 } 

Summary

 
In this article we discussed the record feature introduced in C# 9. We discussed different traits of the record and delved into the compiler generated code to understand the wonders teh compilers does for us. We also saw that the records are internally reference types, but it shares a lot of its equality behavior with structs than classes. It is also different from structs, as it supports inheritence and the fact that it being a reference type, it reduces memory allocations as they would accessed by reference and not by a copy value. Surely, records is one of the most exciting features that would be looked forward to in the C# 9 release. And if one was to evaluate the evolution of language, especially considering the pace it picked up since C# 7 came out, it definitely is a great time to be a C# developer.


Similar Articles