Record Types In C# 9

Getting Setup With C# 9

If you aren’t sure you are using C# 9 and/or you want to start using some of the new shiny features in the C# language, be sure to read our quick guide on getting setup with C# 9 and .NET 5. Any feature written about here is available in the latest C# 9 preview and is not a “theoretical” feature, it’s ready to go!

C# 9 Features

We are slowly working our way through all new C# 9 features, if you are interested in other new additions to the language, check out some of the posts below.

Free C# 9 Video Course

Are you one of those people that sees a big wall of text on a programming blog and instantly tunes out? Well you aren’t alone! In fact, I got hassled so much with people asking me “Can’t you just quickly share your screen and show me?” rather than reading a blog post, I decided I may as well quickly record the need to know bits of C# 9. So that’s what I’ve done!

My “What’s New In C# 9” course is completely free and will get you up and using C# 9 features in less than an hour (seriously!). Did I mention it’s FREE?

Watch For Free Now!

Init-Only Properties In C# 9

Before we go much further, I highly recommend reading our guide on Init-Only Properties in C# 9 here. Record types in C# 9 are borderline an extension of the level of immutability that init-only properties give, and so much of this article will refer back to that.

Record Types Crash Course

The gist of a record type is that it provides an easier way to use the immutability features within C# 9 (init) and provides better equality comparisons…. Or that’s the theory anyway.  I will say that everything within this guide is working right now in the latest SDK. I’ve read a bunch of tutorials that are simply wrong or don’t work.(mini rant) It happens every single C# release where people want to be the first to do a writeup about something that they do guides with “pesudocode”.

I think as well it’s more frustrating than ever with Records because everyone is just copy and pasting the happy path given out in the .NET Core announcements. Rather than actually playing with the code and seeing what it actually does, and where it’s short comings are.

Anyway. Back to records! If we look at a simple record definition like so :

public record Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Now if you wrote code like so, you might expect it to throw an exception (Because everyone is talking about how records are immutable!)  :

var person = new Person();
person.Name = "John Smith";//No Exception

But it doesn’t.

We’ll soon find out how to make it throw an error but it’s a little confusing when you first start using Records because everyone has talked about immutability and how records solve that problem when in reality, they only solve it in very specific scenarios. Depending on which documentation you read, it can be very confusing.

I found a good Github Issue against the dotnet docs here : https://github.com/dotnet/docs/issues/20601 that goes into some of the confusing wording. I think that the idea is that Records *help* you write immutable code, but records themselves are actually not always immutable.

I think the best comment on how to change the docs was this :

It would be good to word it so that no one can come away thinking “if I see a record, I know it’s immutable.”

Which is totally true. Records *can* be immutable, but not always (Like our example above).

To make each property be immutable, with an error, you have to make each property use the init keyword like so :

public record Person
{
    public string Name { get; init; }
    public int Age { get; init; }
}

Now I saw some writeups claim that you can simply do this :

public record Person
{
    string Name;
    int Age;
}

The thinking behind it is that every property on a record is automatically public (Kinda like an interface property), and then every property automatically has a get and init accessor placed on it if you don’t specify. This doesn’t work for me. I don’t know if it worked in a previous version, or people just wrote what they “thought” it should do, but this certainly doesn’t work on the latest SDK.

With that all being said this shorthand way of defining a record *does* work.

public record Person(string Name, int Age);

And this code now throws (two) errors :

var person = new Person(); // Throws an exception because you need to provide the constructor values
person.Name = "John Smith";//Throws an exception because you cannot assign an init only property after creation

There is a small caveat that for some reason, whenever I started writing this syntax, my Visual Studio would crash! This was version 16.7.1, once I upgraded to 16.7.4.. Suddenly I could write shorthand records without Visual Studio restarting on me which was nice.

If you read the arguments around immutability and you hear people say “It’s only immutable when it’s a positional record”, this is what they mean. Why positional? Because it’s dependent on the constructor positioning, for example this does not work :

var person = new Person //Error that we haven't used the constructor and given the Name. 
{
    Name = "John Smith", 
    Age = 30
};

If you want to be able to use the object initializer syntax instead of the constructor syntax, then you need to create your record completely, and provide the init only accessors on your properties that you want to be immutable…. It’s kinda… meh.

It may seem like I’m down on Records, and.. In some ways I am. There are lots of caveats that make me feel like the the entire premise of Records don’t quite work the way I would expect. I’m almost certain that in C# 10, there will be all sorts of improvements and then we will all be happy. Sort of like when (Value)Tuples were added to C# and you had to use .Item1 and Item2 etc and people were pretty horrified, then they added Named Tuples and everything was fine (More reading here : https://dotnetcoretutorials.com/2020/01/06/intro-to-c-tuples/).

Record Equality

All this talk of immutability with Records has meant that the equality features have sort of skipped by. And it’s interesting because you could create records, that are not immutable at all, just to use the equality features.

Let me demonstrate. If we create a class and a record that are essentially identical :

public class PersonClass
{
    public PersonClass(string name, int age)
    {
        this.Name = name;
        this.Age = age;
    }

    public string Name { get; init; }
    public int Age { get; init; }
}

public record Person(string Name, int Age);

Then say we have the following code :

var personClassA = new PersonClass("Jim", 30);
var personClassB = new PersonClass("Jim", 30);

Console.WriteLine(personClassA.Equals(personClassB));

var personRecordA = new Person("Jim", 30);
var personRecordB = new Person("Jim", 30);

Console.WriteLine(personRecordA.Equals(personRecordB));

The output will be False then True. Comparing two classes will check the reference of those two objects. Even if the values within those classes are the same, they will not be equal. Records are different in that equality is done using the values within that record. This is some compiler magic behind the scenes but essentially it’s comparing each of your properties one by one to make sure their *values* are the same.

You can override how a record checks it’s equality… But you cannot do so with the shorthand syntax. You need to write out the full record defintion again :

public record Person : IEquatable
{
    public Person(string name, int age)
    {
        this.Name = name;
        this.Age = age;
    }

    public string Name { get; init; }
    public int Age { get; init; }

    public virtual bool Equals(Person person2)
    {
        return this.Name == person2.Name;
    }
}

And then this code, even with the Age property being different :

var personRecordA = new Person("Jim", 30);
var personRecordB = new Person("Jim", 31);

Console.WriteLine(personRecordA.Equals(personRecordB));

Will still return True.

But this is somewhat daft because at this point, there is very little difference between a class and a record (If any at all to be honest). While records give you helpful ways to do immutability and compare by value.. You have to opt in to some less than ideal scenarios (Must use a constructor, must compare all property values for equality).

I would also note that this also does not work :

var personRecordA = new Person("Jim", 30);
var personRecordB = new Person("Jim", 30);

Console.WriteLine(personRecordA.Equals(personRecordB)); //Returns True
Console.WriteLine(personRecordA == personRecordA); //Returns False

For that you need to override the operator ==, which also requires you to override the operator !=

public record Person : IEquatable
{
    public Person(string name, int age)
    {
        this.Name = name;
        this.Age = age;
    }

    public string Name { get; init; }
    public int Age { get; init; }

    public virtual bool Equals(Person person2)
    {
        return this.Name == person2.Name;
    }

    public static bool operator==(Person person1, Person person2)
    {
        return person1.Equals(person2);
    }

    public static bool operator!=(Person person1, Person person2)
    {
        return !person1.Equals(person2);
    }
}

Note that this is even if you don’t write your own equality operator. Even by default, records do not override the == operator which.. IMO is maybe the wrong decision and probably one that can’t be changed now. But it’s very confusing when personA==personB returns false, but personA.Equals(personB) returns true.

Using The “with” Keyword With Records

Let’s say you have a record with a dozen properties, all init only, and you want to create a new record with only one change. You would have to write out a massively long constructor taking each property from the existing record, and then drop in your one change. Kinda messy.

But there is a helper using the “with” keyword.

var personRecordA = new Person("Jim", 30);
var personRecordB = personRecordA with { Age = 31 };

This means it takes all the values of personRecordA, and creates a new Record with the Age changed. This feature I actually find pretty handy and honestly I wouldn’t be surprised if this was extended to other types (classes and structs) because I think it’s very very handy.

Should You Use Records?

I’ll be the first to admit it’s hard for me to grasp records because in general, immutability hasn’t been big on my wishlist. I personally don’t develop all that much in F#, or use other languages with immutable “record” types, so it’s sometimes hard for my to envisage where I would use them.

But I think that’s made worse by the fact that Records act very differently simply based on whether you use a shorthand syntax or not. The promised features of records actually only happen if you use records in a very precise way, otherwise you’re more or less just using a class. And I think that’s somewhat the problem here, is that the immutability features of records, you can get in a class anyway.

Let me know in the comments where you’ve used Records, or your intended use cases because, as I say, my experience on the need for immutable records is very very small.

ENJOY THIS POST?
Join over 3.000 subscribers who are receiving our weekly post digest, a roundup of this weeks blog posts.
We hate spam. Your email address will not be sold or shared with anyone else.

3 comments

  1. Hello Wade,
    Great article on new C# 9 features.
    Personally, I wait for these features from a long time ago and now I can use them on my DDD development.
    IMHO, it greatly simplify creation of entities, value objects, commands an events…
    However, I would like to have same feature on “struct” but MS have abandoned this 🙁

  2. In the last code snippet

    var personRecordB = personRecordA with { Age = 31 };

    Does that mean properties `personRecordA.Name` and `personRecordB.Name` hold same reference on a string, or `with` expression makes copy of it?
    P.S. Potentially, if this lang feature will be extended on classes we can throw away such libraries as AutoMapper 🙂

Leave a Reply

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