C# 9.0: Records – Work With Immutable Data Classes

In the previous blog posts you learned about different C# 9.0 features:

In this blog post, let’s look at another very interesting feature of C# 9.0 that is called record types or just records.

Working with immutable data is quite powerful, leads often to fewer bugs, and it forces you to transform objects into new objects instead of modifying existing objects. F# developers are used to this, as F# treats everything by default as immutable. Now you get immutable types in C# 9.0 as well, with the so-called record types, or just records. Records make it easier for you to work with immutable data in C#. Before we look at records in this blog post, let’s start with a simple class.

Let’s Start With a Class

In the previous blog post you learned about init-only properties in C# 9.0. I created the Friend class below with the two init-only properties FirstName and LastName. If you don’t know what init-only properties are, please read that previous blog post:

public class Friend
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

With init-only properties you get immutable properties. In the Friend class in the code snippet above, all its properties are init-only properties and so immutable, you can’t change them. That means, when working with this class, you don’t change the property values, because you can’t. If you need to change something, you create a new Friend object with the updated data. This is what you do when working with immutable data. Instead of changing an object over time, you create a new object when a change is needed. This means that your object represents the state of data at a specific point in time.

Let me give you an example of how this is done. Let’s create a Friend object as below:

var friend = new Friend
{
    FirstName = "Thomas",
    LastName = "Huber"
};

Now let’s assume that at some point in your program you need to change the lastname of that friend to Mueller, which is a very common lastname in Germany (You might know the famous soccer scorers Gerd Mueller and Thomas Mueller). As you work with immutable data, you can’t change the LastName property. Instead of doing this, you create a new Friend object that represents the new state. You might create that new Friend object like in the code snippet below. Note how I assign the value of the FirstName property from the first Friend object to the FirstName property of the second Friend object:

var friend = new Friend
{
    FirstName = "Thomas",
    LastName = "Huber"
};


var newFriend = new Friend
{
    FirstName = friend.FirstName,
    LastName = "Mueller"
};

But somehow this approach gets annoying when you have more properties. Let’s add a Middlename property to the Friend class:

public class Friend
{
    public string FirstName { get; init; }
    public string MiddleName { get; init; }
    public string LastName { get; init; }
}

Now you see in the code snippet below that the creation of the second Friend object with a new lastname also gets an additional code line for that new Middlename property. That is because you also have to copy that property from the old Friend object:

var friend = new Friend
{
    FirstName = "Thomas",
    MiddleName = "Claudius",
    LastName = "Huber"
};


var newFriend = new Friend
{
    FirstName = friend.FirstName,
    MiddleName = friend.MiddleName,
    LastName = "Mueller"
};

That means the more properties you have, the harder this gets. Of course you can implement some copy logic with reflection or serialization, or you could use an auto-mapping library. But C# 9.0 has a better way for you to work with immutable data classes: Records.

Create Your First Record

To change our Friend class to a record, you use the record keyword instead of the class keyword. Below you see the corresponding type as a record type:

public record Friend
{
    public string FirstName { get; init; }
    public string MiddleName { get; init; }
    public string LastName { get; init; }
}

Defining the Friend type as a record type means that you want to treat objects of this type like an immutable data value. Defining a type as a record gives you support for the new with expression that is also introduced with C# 9.0.

Create Copies With the With Expression

In the code snippet below you see the way that we used in this blog post to create a new Friend object with a new lastname. This approach works also with the record type, but it is not really efficient: You have to copy all the property values from the original object manually, and you might forget a property in your code if you write it manually as in the snippet below.

var friend = new Friend
{
    FirstName = "Thomas",
    MiddleName = "Claudius",
    LastName = "Huber"
};


var newFriend = new Friend
{
    FirstName = friend.FirstName,
    MiddleName = friend.MiddleName,
    LastName = "Mueller"
};

The with expression allows you to create a new object more efficiently. You see it in action in the code below, and that code leads to the same result as the code that you see in the snippet above. The last statement in the code below uses the with expression to create a new Friend object from the existing Friend object stored in the friend variable. You can read that statement like this: Use the property values of the existing Friend object stored in the friend variable to create a new Friend object and set the LastName property of that new Friend object to Mueller. Store the new Friend object that gets generated by the with expression with the property values shown in the comments in the newFriend variable.

var friend = new Friend
{
    FirstName = "Thomas",
    MiddleName = "Claudius",
    LastName = "Huber"
};

var newFriend = friend with { LastName = "Mueller" };
// newFriend.FirstName is "Thomas"
// newFriend.MiddleName is "Claudius"
// newFriend.LastName is "Mueller"

As you can see in the snippet above, the with expression uses the curly syntax that you know from object initializers to define new values for specific properties. That means if you’re familiar with object initializers, you’ll get up to speed with this new syntax quite fast. But remember, the with expression works only with record types and not with normal classes.

When you work with immutable data, you create a copy of your object for a mutation. This technique is known as non-destructive mutation. Instead of having a single object that represents the state over time, you have immutable objects, each representing the state at a given time.

Check if Your Records are Equal

Record types are reference types and not value types as structs. But their Equals method is implemented for you, so that it compares all the property values for equality. Actually, the C# compiler generates that Equals method for you. And the compiler also generates operator overloads for == and !=, so that these operators use that Equals method too. This is another feature of record types. That means you can compare two records by their property values for equality. The code snippet below shows this in action. First a Friend object is created and stored in the friend variable. Then the with expression is used to create another Friend object from that existing Friend object. The LastName property of the new Friend object is set to Mueller. Then the two Friend objects are compared with the == operator. The result is false, as the LastName property of the new Friend is not Huber, but Mueller.

var friend = new Friend
{
    FirstName = "Thomas",
    LastName = "Huber"
};


var newFriend = friend with { LastName = "Mueller" };

Console.WriteLine(friend == newFriend); // false, as property values are different

var anotherFriend = newFriend with { LastName = "Huber" };

Console.WriteLine(friend == anotherFriend); // true, as property values are the same

After the Console.WriteLine statement a third Friend object is created from the newFriend object. The with expression is used to set the LastName property to Huber, and the created Friend object is stored in the anotherFriend variable. This means that the object stored in the anotherFriend variable has the same property values as the very first Friend object that is stored in the friend variable. Then that very first Friend object is compared with the == operator to that third Friend object stored in the anotherFriend variable. The result is written again to the Console with a Console.WriteLine statement. In this case the result is true, as the properties of the two Friend objects contain the same values.

Checking for equality is another powerful feature of record types. Calling the Equals method or using the == operator compares all the property values. Actually, a record type implements IEquality<T>, in case of our Friend type it implements IEquality<Friend>.

What Does the Compiler Generate?

When you open the .dll file of your app in the Intermediate Language Disassembler (ILDASM.exe – learn how to open it in this post), you can look at the Friend type to see everything that the C# compiler has generated. In the screenshot below you can see our Friend record type in the Intermediate Language Disassembler. The C# compiler actually generated a class for the Friend record type with the properties FirstName, LastName and MiddleName. What you can see there too is the implementation of IEquatable<Friend>. You can also see other things, like for example a copy constructor that takes another Friend object, shown in the screenshot as .ctor : void(class Friend).

When you double click that copy constructor, you can see the Intermediate Language code. The word family on the first line tells you that the constructor is protected. The constructor copies all the values of the passed in Friend object to the new Friend object’s properties FirstName, MiddleName and LastName.

So, you can think of this copy constructor to be something like this in C#:

protected Friend(Friend original)
{
    this.FirstName = original.FirstName;
    this.MiddleName = original.MiddleName;
    this.LastName = original.LastName;
}

When you use the with expression like in the code snippet below, the copy constructor is called to create a new copy of the corresponding Friend object that you specify with the with expression. After that the property LastName is set to the value that you’ve specified in the object initializer of the with expression.

var friend = new Friend
{
    FirstName = "Thomas",
    MiddleName = "Claudius",
    LastName = "Huber"
};

var newFriend = friend with { LastName = "Mueller" };
// newFriend.FirstName is "Thomas"
// newFriend.MiddleName is "Claudius"
// newFriend.LastName is "Mueller"

Beside the copy constructor, there gets more code generated for record types. That compiler-generated code is where all the magic happens. Another useful compiler-generated feature is the protected PrintMembers method. It takes a StringBuilder as a parameter and it adds all the property names and property values to that StringBuilder object. The PrintMembers method is called by the overriden ToString method that is also generated by the compiler. The following code snippet prints the members to the Friend object the console, as the Console.WriteLine method calls the ToString method on the Friend object.

var friend = new Friend
{
    FirstName = "Thomas",
    MiddleName="Claudius",
    LastName = "Huber"
};

Console.WriteLine(friend);

The output at the console looks like below. As you can see, first the type is printed and then all the members:

Friend { FirstName = Thomas, MiddleName = Claudius, LastName = Huber }

Hey Thomas, I Don’t Want to Read IL Code

Ok, got it. So, let’s decompile the IL Code again to C# code. Let’s look at the C# code that gets generated for this record type:

public record Friend
{
    public string FirstName { get; init; }
    public string MiddleName { get; init; }
    public string LastName { get; init; }
}

To decompile the created C# code in the compiled .dll of the application I use ILSpy. Below you see the decompiled output that I get for the Friend type. There you can see the copy constructor, the PrintMembers method, the override of the ToString method, the generated Equals method, the overloaded == and != operators and much more.

Note that the attributes NullabeContext and Nullable from the System.Runtime.CompilerServices namespace are added by the C# compiler for nullable reference types that where introduced with C# 8.0. Don’t worry about these attributes when looking at the class. Just ignore the attributes and read the logic in the constructor and in the different methods. If there are questions, just comment under this blog post

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;

public class Friend : IEquatable<Friend>
{
    [System.Runtime.CompilerServices.Nullable(1)]
    protected virtual Type EqualityContract
    {
        [System.Runtime.CompilerServices.NullableContext(1)]
        [CompilerGenerated]
        get
        {
            return typeof(Friend);
        }
    }

    public string FirstName { get; init; }

    public string MiddleName { get; init; }

    public string LastName { get; init; }

    public override string ToString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("Friend");
        stringBuilder.Append(" { ");
        PrintMembers(stringBuilder);
        stringBuilder.Append(" } ");
        return stringBuilder.ToString();
    }

    [System.Runtime.CompilerServices.NullableContext(1)]
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append("FirstName");
        builder.Append(" = ");
        builder.Append((object)FirstName);
        builder.Append(", ");
        builder.Append("MiddleName");
        builder.Append(" = ");
        builder.Append((object)MiddleName);
        builder.Append(", ");
        builder.Append("LastName");
        builder.Append(" = ");
        builder.Append((object)LastName);
        return true;
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public static bool operator !=(Friend r1, Friend r2)
    {
        return !(r1 == r2);
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public static bool operator ==(Friend r1, Friend r2)
    {
        return (object)r1 == r2 || (r1?.Equals(r2) ?? false);
    }

    public override int GetHashCode()
    {
        return ((EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295
              + EqualityComparer<string>.Default.GetHashCode(FirstName)) * -1521134295
              + EqualityComparer<string>.Default.GetHashCode(MiddleName)) * -1521134295
              + EqualityComparer<string>.Default.GetHashCode(LastName);
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public override bool Equals(object obj)
    {
        return Equals(obj as Friend);
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public virtual bool Equals(Friend other)
    {
        return (object)other != null
             && EqualityContract == other.EqualityContract 
             && EqualityComparer<string>.Default.Equals(FirstName, other.FirstName)
             && EqualityComparer<string>.Default.Equals(MiddleName, other.MiddleName)
             && EqualityComparer<string>.Default.Equals(LastName, other.LastName);
    }

    [System.Runtime.CompilerServices.NullableContext(1)]
    public virtual Friend <Clone>$()
    {
        return new Friend(this);
    }

    protected Friend([System.Runtime.CompilerServices.Nullable(1)] Friend original)
    {
        FirstName = original.FirstName;
        MiddleName = original.MiddleName;
        LastName = original.LastName;
    }

    public Friend()
    {
    }
}

So, this code snippet above makes it clear that the magic of record types is a lot of compiler-generated code. And there’s even more that can be compiler-generated, so let’s continue.

Create a Constructor and a Deconstructor

Maybe you want to work with your records with a constructor instead of using object initializers. And maybe you also want to have positional deconstruction. Then you can implement a constructor and a Deconstruct method. The following Friend record does this:

public record Friend
{
    public string FirstName { get; init; }
    public string MiddleName { get; init; }
    public string LastName { get; init; }

    public Friend(string firstName, string middleName, string lastName)
    {
        FirstName = firstName;
        MiddleName = middleName;
        LastName = lastName;
    }

    public void Deconstruct(out string firstName,
        out string middleName, out string lastName)
    {
        firstName = FirstName;
        middleName = MiddleName;
        lastName = LastName;
    }
}

Now, with the constructor and the Deconstruct method in place, you can construct and deconstruct your Friend type like this:

var friend = new Friend("Thomas", "Claudius", "Huber"); // This is the construction

var (first, middle, last) = friend; // This is the deconstruction
Console.WriteLine(first); // Thomas
Console.WriteLine(middle); // Claudius
Console.WriteLine(last); // Huber

As you can see, the deconstruction allows you to assign the Friend object to a tuple that specifies the individual variables to store the values. If you’re not interested in a value, you can use a discard, which is an underscore. If you don’t need the middlename for example, you can write the tuple like this: (first, _, last).

The Deconstruct method is supported since C# 7.0. It was introduced with the concept of Tuples. You can add a public Deconstruct method to any type in .NET. It must return void and have only out parameters. Then you can use the deconstruction syntax to deconstruct the type to a tuple as shown in the snippet above. Read more about Tuples and the Deconstruct method in the official docs.

Now the important thing is that all that construction and deconstruction would also work if the Friend type would be a class instead of a record. The power of record types – beside the with expression and generated members like the overridden ToString method – is that the C# compiler can also generate all that constructor and deconstructor boilerplate for you, including the init-only properties.

Generate Constructor, Deconstructor and Init-only Properties

Look at this beautiful piece of code below. This is a short-hand syntax to create a record type. This syntax is also known as a Positional Record:

public record Friend(string FirstName,string MiddleName, string LastName);

The code snippet above creates a Friend record type with the public init-only properties FirstName, MiddleName, LastName AND it creates a public constructor AND it creates a Deconstruct method. Isn’t that powerful? Note that the parameters in the code snippet above are written in PascalCase, which means they start with an uppercase character. PascalCase instead of camelCase is used, because properties are generated from these parameters, and properties in .NET are written in PascalCase.

With a constructor you have to pass in the values by position to create an object. That is a positional object creation. When you use an object initializer, the order of the properties does not matter. That’s a so-called nominal object creation. The record defined above creates a constructor that takes all the property values as parameters. That is the reason why it is called a positional record. You have to pass in all the values by position to the constructor to create an object.

When you look at the generated Friend type in ILDASM in the screenshot below, you can see that the properties FirstName, MiddleName, and LastName were generated for the positional record. Also a public constructor with three string parameters is generated. These parameters are for firstname, middlename and lastname. Also the Deconstruct method is generated for you, beside all the other stuff that you saw already before, like for example the protected copy constructor that has a single Friend parameter.

The code snippet below shows a full top-level program that uses the short-hand syntax to define the record type Friend and that constructs and deconstructs a Friend object. As the Friend type is a record type, the Friend object is of course also immutable.

using System;

var friend = new Friend("Thomas", "Claudius", "Huber");

Console.WriteLine(friend.FirstName); // Thomas

var (first, middle, last) = friend; // deconstruct
Console.WriteLine(first); // Thomas
Console.WriteLine(middle); // Claudius
Console.WriteLine(last); // Huber

public record Friend(string FirstName,string MiddleName, string LastName);

Positional Records and the With Expression

When you use a positional record like the one below, you get a constructor with three parameters, and there’s no default constructor anymore:

public record Friend(string FirstName,string MiddleName, string LastName);

So, you might think that you can’t use the with expression anymore, as there’s no default constructor. But actually, the protected copy constructor still exists, and that one is used by the with expression. That means, also a positional record like the one created above can be used with the with expression like in the following top-level program:

using System;

var friend = new Friend("Thomas", "Claudius", "Huber" );

var newFriend = friend with { LastName = "Mueller" };

Console.WriteLine(newFriend);

public record Friend(string FirstName, string MiddleName, string LastName);

The output at the console is this:

Friend { FirstName = Thomas, MiddleName = Claudius, LastName = Mueller }

Inherit Records from Other Records

Inheritance works also with record types. You can inherit your records from other records or from object. You can not inherit a record from any other class than object, and you can not inherit a class from a record. The following code snippet shows two record types. The record Developer inherits from the record Person.

public record Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

public record Developer : Person
{
    public string FavoriteLanguage { get; init; }
}

Everything works as you would expect it. Let’s try some typical stuff that you do usually when you work with inheritance. For example, let’s store a Developer object in a Person variable. As you can see in the screenshot below, the with expression let’s you only set the properties FirstName and LastName that are available in the Person class. The FavoriteLanguage property is not available in the object initializer, as it is defined in the Developer class, but we have here a Person variable.

Let’s set the FirstName property to Tom like you see it in the code snippet below. Now the question is, what does the with expression create: A new Person object or a new Developer object?

Person person = new Developer
{
    FirstName = "Thomas",
    LastName = "Huber",
    FavoriteLanguage = "C#"
};

var newPerson = person with { FirstName = "Tom" };

The with expression is smart enough to find out at runtime that the person variable of type Person contains a Developer object, so it creates a new Developer object and not a new Person object. The code snippet below writes the value of the FavoriteLanguage property of that new Developer object to the Console.

Person person = new Developer
{
    FirstName = "Thomas",
    LastName = "Huber",
    FavoriteLanguage = "C#"
};

var newPerson = person with { FirstName = "Tom" };

if (newPerson is Developer newDev) // is true, as newPerson variable contains a Developer
{
    Console.WriteLine(newDev.FavoriteLanguage); // C#
}

How is this possible that the with expression is so smart? The with expression actually calls the copy constructor to create a new object via the compiler-generated <Clone>$ method. In the code snippet below you can see the decompiled <Clone>$ method for the Person record type:

public virtual Person <Clone>$()
{
    return new Person(this);
}

As you can see in the snippet above, the <Clone>$ method calls internally the copy constructor to clone the current instance. Now, the interesting bit is that this <Clone>$ method is virtual. And now you can guess what the decompiled <Clone>$ method of our Developer type that inherits from Person looks like. Here we go:

public override Person <Clone>$()
{
    return new Developer(this);
}

As you can see in the code snippet above, the generated code for the Developer record type overrides the <Clone>$ method of the Person type, and internally it creates and returns a new Developer instance. It allows the with expression to create a Developer copy, even if the original Developer instance is stored in a Person variable like in our code above. Because then, the overridden <Clone>$ method of the Developer class is called that calls the Developer constructor. This is the whole magic behind the with expression when it is used with inheritance like we did in this section. If you’re familiar with object-oriented programming, you know that this override of the <Clone>$ method is called polymorphism.

Inheritance and Equality

Record types work also as expected when you use inheritance and when you check for equality. In the code snippet below I create a new Person and a new Developer, both have the same values for the properties FirstName and LastName. But when you compare the two objects with the == operator, you get false as a result, because the types Person and Developer are different types.

var person = new Person
{
    FirstName = "Thomas",
    LastName = "Huber",
};

var dev = new Developer
{
    FirstName = "Thomas",
    LastName = "Huber",
};

Console.WriteLine(person == dev); // false, because types are different

This means that beside the property values, the Equals method and the overloaded == operator of a record type also take the type into account. Let’s take a look at how this works. When you decompile the generated code, you find on the Person type this readonly property called EqualityContract. As you can see, it returns typeof(Person), and it is also a virtual property.

[System.Runtime.CompilerServices.Nullable(1)]
protected virtual Type EqualityContract
{
    [System.Runtime.CompilerServices.NullableContext(1)]
    [CompilerGenerated]
    get
    {
        return typeof(Person);
    }
}

The sub type Developer overrides that EqualityContract property, you can see that override in the code snippet below. As you can see, the override returns typeof(Developer).

[System.Runtime.CompilerServices.Nullable(1)]
protected override Type EqualityContract
{
    [System.Runtime.CompilerServices.NullableContext(1)]
    [CompilerGenerated]
    get
    {
        return typeof(Developer);
    }
}

Now, when you look at the decompiled Equals methods in the Person type, you can see that the Equals method that takes a Person as a parameter first compares the EqualityContract properties before it compares the values of the FirstName and LastName properties. That’s the reason why a Person instance and a Developer instance will never be equal.

[System.Runtime.CompilerServices.NullableContext(2)]
public override bool Equals(object obj)
{
    return Equals(obj as Person);
}

[System.Runtime.CompilerServices.NullableContext(2)]
public virtual bool Equals(Person other)
{
    return (object)other != null
        && EqualityContract == other.EqualityContract
        && EqualityComparer<string>.Default.Equals(FirstName, other.FirstName)
        && EqualityComparer<string>.Default.Equals(LastName, other.LastName);
}

When you look at the decompiled Equals methods of the Developer class in the code snippet below, you can see that the Developer type overrides the two Equals methods from the Person type (Actually, the very first Equals method is from System.Object). The second Equals method calls the first Equals method. The first Equals method calls the third Equals method. That means, the third Equals method gets always called. There you can see that it first calls base.Equals, which is the Equals implementation in the Person base type that you see in the code snippet above. If that base.Equals method returns true, the Equals method of the Developer type also compares the value of the FavoriteLanguage property that is defined in the Developer record type.

[System.Runtime.CompilerServices.NullableContext(2)]
public override bool Equals(object obj)
{
    return Equals(obj as Developer);
}

[System.Runtime.CompilerServices.NullableContext(2)]
public sealed override bool Equals(Person other)
{
    return Equals((object)other);
}

[System.Runtime.CompilerServices.NullableContext(2)]
public virtual bool Equals(Developer other)
{
    return base.Equals(other) 
      && EqualityComparer<string>.Default.Equals(FavoriteLanguage, other.FavoriteLanguage);
}

So, everything you need for your record types regarding equality checks is generated for you by the C# compiler, also when you’re using inheritance as you saw in this section.

Use Positional Records and Inheritance

You can also use positional records and inheritance. Below you see the types Person and Developer. Developer inherits from Person. The Developer constructor passes the values of the FirstName and LastName parameter to the Person constructor.

public record Person(string FirstName, string LastName);

public record Developer (string FirstName, string LastName, string FavoriteLanguage)
  : Person(FirstName, LastName);

Now, when you look at the code snippet above, you could think that the properties FirstName and LastName are generated on the Person type and on the Developer type. But actually, the C# compiler is a smart beast. Below you see the Developer type in the Intermediate Language Disassembler. As you can see, it contains only the FavoriteLanguage property, but not the properties FirstName and LastName. Those are defined in the base type Person.

In the screenshot above you can also see the generated constructor (.ctor) with the three string parameters. When you double-click that constructor, you see the Intermediate Language code like in the screenshot below. You can see there that the constructor of the Developer type has the three string parameters FirstName, LastName and FavoriteLanguage. In the constructor, the FavoriteLanguage property is set, and after that the base constructor of the Person type is called.

When you decompile the Intermediate Language code with the Developer constructor with a tool like ILSpy, you see that the C# variant of such a constructor looks like below. Note the call to the base constructor defined in the Person type that takes the firstname and the lastname as arguments:

public Developer(string FirstName, string LastName, string FavoriteLanguage)
  : base(FirstName, LastName)
{
	this.FavoriteLanguage = FavoriteLanguage;
}

So, positional records work also well with inheritance. Below you see a full-blown top-level program that shows the Person and the Developer type in action.

using System;
using System.Collections.Generic;

var persons = new List<Person>
{
    new Person("Julia","Huber"),
    new Developer("Thomas", "Huber", "C#"),
};

foreach(var person in persons)
{
    Console.WriteLine(person);
}

public record Person(string FirstName, string LastName);

public record Developer (string FirstName, string LastName, string FavoriteLanguage)
  : Person(FirstName, LastName);

The console output looks like this:

Person { FirstName = Julia, LastName = Huber }
Developer { FirstName = Thomas, LastName = Huber, FavoriteLanguage = C# }

Summary

You learned in this blog post about record types that are introduced with C# 9.0. They make working with immutable data objects in C# a joy. For F# developers this is nothing new, but for C# developers it’s a huge improvement to the language. The with expression is a powerful and nice syntax that is only available for records, and also the positional records are great to generate properties and constructors/deconstructors.

Happy coding,
Thomas

Share this post

Comments (9)

  • Jan Reply

    Hi Thomas,

    thanks for this blog post. It was worth every word :)

    One question I am curious is what happens if you use other records or classes as field-type (e. g. an Adress field) or even a list of adresses. Is this also supported and does the with-expression create deep copies?)

    best regards, Jan

    September 11, 2020 at 6:57 am
    • Alex Reply

      Jan, I’m also curious how it can be done using records in C#9 but couldn’t find anything.
      I know you can do that with “normal” immutable classes using remute library – https://github.com/ababik/Remute
      So you can do `person.With(x => x.Address.Street1, “….”);`
      Would be great to do it out of the box though. I guess “record” means flat structure.

      September 22, 2020 at 4:26 am
  • Mr Richard J Bushnell Reply

    Really helpful that Thomas, thanks! Enjoyed reading it.

    Do you know if there’s a way to generate structs instead of classes? Just thinking that if you were to do this indiscriminately with a lot of objects, you could end up with an awful lot of copied objects filling up the heap.

    Cheers.

    September 17, 2020 at 4:10 pm
    • Thomas Claudius Huber Reply

      Thanks Richard. I haven’t heard about any compiler flag that would allow you to create structs instead of classes, but I like that idea.

      September 17, 2020 at 4:15 pm
  • Shmuel Reply

    Really clear and helpful explanation of record types – thanks Thomas!
    When using positional records, is there a way to still include an init code block to be able to add e.g. validation logic to the properties? And would this be possible to do individually for each property?
    Thanks

    November 10, 2020 at 11:51 am
  • Meir Kriheli Reply

    I guess no way to force a type to be immutable. Not even using record.
    A developer can still need to make sure not to use properties like:
    public List MutableProperty { get; init; }
    Correct?
    Isn’t there some attribute or different keyword to make VS worn when doig such nested mistakes?
    Maybe like
    public immutable record SomeImmutableRecord {}

    November 11, 2020 at 10:09 pm
    • Thomas Claudius Huber Reply

      Not sure I get the point. Yes, a developer can use other properties. But records are a tool for developers, and they can use it or not. So, if the developer decides to create a record with no init-only properties, that’s their decision. :) There’s no other keyword.

      November 23, 2020 at 10:08 am
  • Christof Reply

    @Meir Kriheli, I agree it was a valuable feature having the compiler assert for “deep immutability”, i.e. only allow properties which are immutable themselves.
    What I find most unfortunate though, is the fact records still allow for traditional properties to be contained, exposing public setters, like any another reference type.
    @Thomas, nice post – thank you!

    December 20, 2020 at 12:34 pm

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.