“Records are immutable”.
I often get this answer from people asking: “What do you know about Records?”. That’s it, nothing else.
Records were introduced in C# 9.0 and released in November 2020 as part of the .NET 5.0 release. It’s almost been four years of having them!
It’s not a complete answer. First, it’s only partially accurate that records are immutable. Second, Records have more features. So, let’s dive into the details!
Positional Records
Many people could not answer what positional records are, even though they answered that Records are immutable.
I bet you have seen Positional Records many times and probably use them all the time.
public record Person(string Name, string Surname, int Age);
In the example, we use a primary constructor for the Record. The compiler will generate public properties with init modifiers for us. The parameters in the primary constructor are called positional parameters. The compiler creates positional properties that mirror the positional parameters. That’s why they called Positional Records.
Immutability
“Records are immutable”.
That’s not a complete answer. First, you must specify that you’re talking about Positional Records because we can define mutable records.
public record Person
{
public string Name { get; set; }
public string Surname { get; set; }
public int Age { get; set; }
}
Also, the properties defined with the init access modifier (by the compiler or you) have only shallow immutability.
With init you can’t change the value or the reference of reference type properties. But you can change the data that a reference type property refers to.
Let’s see the example with the following Record.
public record Post( string Name, string Content, string[] Tags);
Our Post has an array of Tags. After initialization, we can’t change the Tags with a new array.
var post = new Post("My Post", "My Content", ["tag1", "tag2"]);
post.Tags = ["tag3"];
However, we can change the values of existing Tags 🙂
var post = new Post("My Post", "My Content", ["tag1", "tag2"]);
post.Tags[0] = "tag3";
Console.WriteLine(post.Tags[0]);
// tag3
It’s important to remember that Records do not have deep immutability but shallow immutability.
Value Equality
When Records were released, they were reference types. Since C# 10, we can also create Records as value types.
// Reference type
public record Person(string Name, string Surname, int Age);
// Reference type
public record class Person(string Name, string Surname, int Age);
// Value type
public record struct Person(string Name, string Surname, int Age);
The first option creates a reference type by default. The .NET team left it for backward compatibility.
But let’s go back to reference types. As we know, the reference types are compared by references in .NET. Records are different. They are compared by values.
var johnDoe1 = new Person("John", "Doe", 30);
var johnDoe2 = new Person("John", "Doe", 30);
Console.WriteLine(johnDoe1 == johnDoe2);
// True
Nondestructive Mutations
We can create a new instance of the Record with some modifications. It’s called nondestructive mutation. We can do it with a with expression.
var johnDoe = new Person("John", "Doe", 30);
var janeDoe = johnDoe with { Name = "Jane", Age = 27 };
We created a new instance janeDoe from johnDoe changing only Name and Age properties. The Surname property is copied.
Compiler-generated ToString()
The compiler generates a built-in display of the Record properties for us. It’s beneficial because you know that the not overridden ToString() method in classes and structures returns only the type name.
The format is the following.
<record type name> { <property name> = <value> }
Console.WriteLine(johnDoe);
// Person { Name = John, Surname = Doe, Age = 30 }
Console.WriteLine(janeDoe);
// Person { Name = Jane, Surname = Doe, Age = 27 }
Deconstructing
We can deconstruct the Record into variables. However, we can do it only with Positional Records! It doesn’t work for usual Records.
var (name, surname, age) = johnDoe;
Console.WriteLine(name); // John
Console.WriteLine(surname); // Doe
Console.WriteLine(age); // 30
Pattern Matching
The Positional Records work seamlessly with C#’s pattern-matching syntax. They can be used in switch expressions to destructure and match on properties easily.
var white = new Color(255, 255, 255);
var color = white switch
{
Color(255, 255, 255) => "White",
Color(0, 0, 0) => "Black",
Color(255, 0, 0) => "Red",
Color(0, 255, 0) => "Green",
Color(0, 0, 255) => "Blue",
_ => "Unknown"
};
Console.WriteLine(color); // White
public record Color(int Red, int Green, int Blue);
More about Pattern Matching read in my other post.
Use cases
We can use them for DTOs, Events, Commands, and other immutable models.
Records are suitable for representing data received from external APIs or for deserializing JSON objects into strongly typed structures.
Records are helpful when you want instances with the same values to be considered equal.
Records can be used to create lightweight, immutable representations of events or messages when logging or tracing information.
However, Records are not suitable for representing entity types in Entity Framework.
Summary
Remember to mention Positional Records when saying that Records are immutable by default. Also, Records have shallow immutability.
The Records is an excellent feature in C#. They are powerful when we understand all their features.
Oh, wait! Records support inheritance, like classes. But it’s a story for another post 🙂