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.
- 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
- // Partial generated code
- public class User
- {
- public string UserName{get;init;}
- public int Id{get;init;}
- public User(string userName,int id)
- {
- (UserName,Id) = (userName,id);
- }
- }
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,
- public record Person
- {
- public string UserName { get; init; }
- public int Id { get; init; }
- }
Structural Equality
- public class User : IEquatable<User>
- {
- public string UserName{get;init;}
- public int Id{get;init;}
- public User(string userName,int id)
- {
- (UserName,Id) = (userName,id);
- }
- public override int GetHashCode()
- {
- return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(UserName)) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(Id);
- }
- public override bool Equals(object? obj)
- {
- return Equals(obj as User);
- }
- [System.Runtime.CompilerServices.NullableContext(2)]
- public static bool operator !=(User? r1, User? r2)
- {
- return !(r1 == r2);
- }
- [System.Runtime.CompilerServices.NullableContext(2)]
- public static bool operator ==(User? r1, User? r2)
- {
- return (object)r1 == r2 || (r1?.Equals(r2) ?? false);
- }
- }
- var user1 = new User("Anu Viswan", 1);
- var user2 = new User("Anu Viswan", 1);
- var user3 = new User("Manu Viswan", 1);
- // Structural Equality
- Console.WriteLine($"user1==user2 : {user1 == user2}");
- Console.WriteLine($"user1==user3 : {user1 == user3}");
- // Output
- // user1==user2 : True
- // user1==user3 : False
Deconstructor
- var user = new User("Anu Viswan", 1);
- var (userName, id) = user;
ToString()
- var user = new User("Anu Viswan", 1);
- Console.WriteLine(user.ToString());
- // Output
- // 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.
- public class User: IEquatable < User > {
- protected virtual Type EqualityContract {
- /* Code Hidden */ }
- public string UserName {
- get;
- init;
- }
- public int Id {
- get;
- init;
- }
- public User(string UserName, int Id) {
- /* Code Hidden */ }
- public override string ToString() {
- /* Code Hidden */ }
- protected virtual bool PrintMembers(StringBuilder builder) {
- /* Code Hidden */ }
- [System.Runtime.CompilerServices.NullableContext(2)]
- public static bool operator != (User ? r1, User ? r2) {
- /* Code Hidden */ }
- [System.Runtime.CompilerServices.NullableContext(2)]
- public static bool operator == (User ? r1, User ? r2) {
- /* Code Hidden */ }
- public override int GetHashCode() {
- /* Code Hidden */ }
- public override bool Equals(object ? obj) {
- /* Code Hidden */ }
- public virtual bool Equals(User ? other) {
- /* Code Hidden */ }
- public virtual User < Clone > $() {
- /* Code Hidden */ }
- protected User(User original) {
- /* Code Hidden */ }
- public void Deconstruct(out string UserName, out int Id) {
- /* Code Hidden */ }
- }
Inheritence
- public record User(string UserName, int Id);
- 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#.
- public record Person{}
- // Following would not compiler - A class cannot inherit from a Record
- public class Employee: Person {}
- // Same is case here, invalid - A record can inherit only from another record
- public class Foo { }
- public record Bar : Foo {}
- A record can inherit only from another record
- A class cannot inherit from a record
- public record User(string UserName, int Id);
- public record Customer(string UserName,int Id,string Location):User(UserName, Id);
- public record Employee(string DisplayName,int Id,string Location):User(DisplayName, Id);
Id and Location. Two of them are being inherited from the base record, while the third one, Location, is part of the Customer record.- var user = new User("Anu Viswan",1);
- var customer = new Customer("Anu Viswan", 1, "India");
- var employee = new Employee("Anu Viswan", 1, "India");
- Console.WriteLine(user.ToString());
- Console.WriteLine(customer.ToString());
- Console.WriteLine(employee.ToString());
- // Output
- User { UserName = Anu Viswan, Id = 1 }
- Customer { UserName = Anu Viswan, Id = 1, Location = India }
- 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.
- public void Deconstruct(out string DisplayName, out int Id, out string Location)
- {
- DisplayName = this.DisplayName;
- Id = base.Id;
- Location = this.Location;
- }
With Keyword
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.
- var anotherUser = user with
- {
- UserName = "Manu Viswan"
- };
- // Output
- 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.
Custom Cloning
- public record User(string UserName, int Id)
- {
- public User(User original) => (UserName, Id) = (original.UserName, -1);
- }
Now exucuting our previous code, would generate the following output.
- User { UserName = Manu Viswan, Id = -1 }

Mahesh ChandPosted Jan 2, 2022, 2:18 AM
Nice. Thank you, Anu. Best