C# 9.0 on the record

Mads Torgersen

C# 9.0 on the record

It’s official: C# 9.0 is out! Back in May I blogged about the C# 9.0 plans, and the following is an updated version of that post to match what we actually ended up shipping.

With every new version of C# we strive for greater clarity and simplicity in common coding scenarios, and C# 9.0 is no exception. One particular focus this time is supporting terse and immutable representation of data shapes.

Init-only properties

Object initializers are pretty awesome. They give the client of a type a very flexible and readable format for creating an object, and they are especially great for nested object creation where a whole tree of objects is created in one go. Here’s a simple one:

var person = new Person { FirstName = "Mads", LastName = "Torgersen" };

Object initializers also free the type author from writing a lot of construction boilerplate – all they have to do is write some properties!

public class Person
{
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
}

The one big limitation today is that the properties have to be mutable for object initializers to work: They function by first calling the object’s constructor (the default, parameterless one in this case) and then assigning to the property setters. Init-only properties fix that! They introduce an init accessor that is a variant of the set accessor which can only be called during object initialization:

public class Person
{
    public string? FirstName { get; init; }
    public string? LastName { get; init; }
}

With this declaration, the client code above is still legal, but any subsequent assignment to the FirstName and LastName properties is an error:

var person = new Person { FirstName = "Mads", LastName = "Nielsen" }; // OK
person.LastName = "Torgersen"; // ERROR!

Thus, init-only properties protect the state of the object from mutation once initialization is finished.

Init accessors and readonly fields

Because init accessors can only be called during initialization, they are allowed to mutate readonly fields of the enclosing class, just like you can in a constructor.

public class Person
{
    private readonly string firstName = "<unknown>";
    private readonly string lastName = "<unknown>";
    
    public string FirstName 
    { 
        get => firstName; 
        init => firstName = (value ?? throw new ArgumentNullException(nameof(FirstName)));
    }
    public string LastName 
    { 
        get => lastName; 
        init => lastName = (value ?? throw new ArgumentNullException(nameof(LastName)));
    }
}

Records

At the core of classic object-oriented programming is the idea that an object has strong identity and encapsulates mutable state that evolves over time. C# has always worked great for that, But sometimes you want pretty much the exact opposite, and here C#’s defaults have tended to get in the way, making things very laborious.

If you find yourself wanting the whole object to be immutable and behave like a value, then you should consider declaring it as a record:

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

A record is still a class, but the record keyword imbues it with several additional value-like behaviors. Generally speaking, records are defined by their contents, not their identity. In this regard, records are much closer to structs, but records are still reference types.

While records can be mutable, they are primarily built for better supporting immutable data models.

With-expressions

When working with immutable data, a common pattern is to create new values from existing ones to represent a new state. For instance, if our person were to change their last name we would represent it as a new object that’s a copy of the old one, except with a different last name. This technique is often referred to as non-destructive mutation. Instead of representing the person over time, the record represents the person’s state at a given time. To help with this style of programming, records allow for a new kind of expression; the with-expression:

var person = new Person { FirstName = "Mads", LastName = "Nielsen" };
var otherPerson = person with { LastName = "Torgersen" };

With-expressions use object initializer syntax to state what’s different in the new object from the old object. You can specify multiple properties.

The with-expression works by actually copying the full state of the old object into a new one, then mutating it according to the object initializer. This means that properties must have an init or set accessor to be changed in a with-expression.

Value-based equality

All objects inherit a virtual Equals(object) method from the object class. This is used as the basis for the Object.Equals(object, object) static method when both parameters are non-null. Structs override this to have "value-based equality", comparing each field of the struct by calling Equals on them recursively. Records do the same. This means that in accordance with their "value-ness" two record objects can be equal to one another without being the same object. For instance if we modify the last name of the modified person back again:

var originalPerson = otherPerson with { LastName = "Nielsen" };

We would now have ReferenceEquals(person, originalPerson) = false (they aren’t the same object) but Equals(person, originalPerson) = true (they have the same value). Along with the value-based Equals there’s also a value-based GetHashCode() override to go along with it. Additionally, records implement IEquatable<T> and overload the == and != operators, so that the value-based behavior shows up consistently across all those different equality mechanisms.

Value equality and mutability don’t always mesh well. One problem is that changing values could cause the result of GetHashCode to change over time, which is unfortunate if the object is stored in a hash table! We don’t disallow mutable records, but we discourage them unless you have thought through the consequences!

Inheritance

Records can inherit from other records:

public record Student : Person
{
    public int ID;
}

With-expressions and value equality work well with record inheritance, in that they take the whole runtime object into account, not just the type that it’s statically known by. Say that I create a Student but store it in a Person variable:

Person student = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 129 };

A with-expression will still copy the whole object and keep the runtime type:

var otherStudent = student with { LastName = "Torgersen" };
WriteLine(otherStudent is Student); // true

In the same manner, value equality makes sure the two objects have the same runtime type, and then compares all their state:

Person similarStudent = new Student { FirstName = "Mads", LastName = "Nielsen", ID = 130 };
WriteLine(student != similarStudent); //true, since ID's are different

Positional records

Sometimes it’s useful to have a more positional approach to a record, where its contents are given via constructor arguments, and can be extracted with positional deconstruction. It’s perfectly possible to specify your own constructor and deconstructor in a record:

public record Person 
{ 
    public string FirstName { get; init; } 
    public string LastName { get; init; }
    public Person(string firstName, string lastName) 
      => (FirstName, LastName) = (firstName, lastName);
    public void Deconstruct(out string firstName, out string lastName) 
      => (firstName, lastName) = (FirstName, LastName);
}

But there’s a much shorter syntax for expressing exactly the same thing (modulo casing of parameter names):

public record Person(string FirstName, string LastName);

This declares the public init-only auto-properties and the constructor and the deconstructor, so that you can write:

var person = new Person("Mads", "Torgersen"); // positional construction
var (f, l) = person;                        // positional deconstruction

If you don’t like the generated auto-property you can define your own property of the same name instead, and the generated constructor and deconstructor will just use that one. In this case, the parameter is in scope for you to use for initialization. Say, for instance, that you’d rather have the FirstName be a protected property:

public record Person(string FirstName, string LastName)
{
    protected string FirstName { get; init; } = FirstName; 
}

A positional record can call a base constructor like this:

public record Student(string FirstName, string LastName, int ID) : Person(FirstName, LastName);

Top-level programs

Writing a simple program in C# requires a remarkable amount of boilerplate code:

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("Hello World!");
    }
}

This is not only overwhelming for language beginners, but clutters up the code and adds levels of indentation. In C# 9.0 you can just write your main program at the top level instead:

using System;

Console.WriteLine("Hello World!");

Any statement is allowed. The program has to occur after the usings and before any type or namespace declarations in the file, and you can only do this in one file, just as you can have only one Main method today. If you want to return a status code you can do that. If you want to await things you can do that. And if you want to access command line arguments, args is available as a "magic" parameter.

using static System.Console;
using System.Threading.Tasks;

WriteLine(args[0]);
await Task.Delay(1000);
return 0;

Local functions are a form of statement and are also allowed in the top level program. It is an error to call them from anywhere outside of the top level statement section.

Improved pattern matching

Several new kinds of patterns have been added in C# 9.0. Let’s look at them in the context of this code snippet from the pattern matching tutorial:

public static decimal CalculateToll(object vehicle) =>
    vehicle switch
    {
       ...
       
        DeliveryTruck t when t.GrossWeightClass > 5000 => 10.00m + 5.00m,
        DeliveryTruck t when t.GrossWeightClass < 3000 => 10.00m - 2.00m,
        DeliveryTruck _ => 10.00m,

        _ => throw new ArgumentException("Not a known vehicle type", nameof(vehicle))
    };

Simple type patterns

Previously, a type pattern needs to declare an identifier when the type matches – even if that identifier is a discard _, as in DeliveryTruck _ above. But now you can just write the type:

DeliveryTruck => 10.00m,

Relational patterns

C# 9.0 introduces patterns corresponding to the relational operators <, <= and so on. So you can now write the DeliveryTruck part of the above pattern as a nested switch expression:

DeliveryTruck t when t.GrossWeightClass switch
{
    > 5000 => 10.00m + 5.00m,
    < 3000 => 10.00m - 2.00m,
    _ => 10.00m,
},

Here > 5000 and < 3000 are relational patterns.

Logical patterns

Finally you can combine patterns with logical operators and, or and not, spelled out as words to avoid confusion with the operators used in expressions. For instance, the cases of the nested switch above could be put into ascending order like this:

DeliveryTruck t when t.GrossWeightClass switch
{
    < 3000 => 10.00m - 2.00m,
    >= 3000 and <= 5000 => 10.00m,
    > 5000 => 10.00m + 5.00m,
},

The middle case there uses and to combine two relational patterns and form a pattern representing an interval. A common use of the not pattern will be applying it to the null constant pattern, as in not null. For instance we can split the handling of unknown cases depending on whether they are null:

not null => throw new ArgumentException($"Not a known vehicle type: {vehicle}", nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))

Also not is going to be convenient in if-conditions containing is-expressions where, instead of unwieldy double parentheses:

if (!(e is Customer)) { ... }

You can just say

if (e is not Customer) { ... }

And in fact, in an is not expression like that we allow you to name the Customer for subsequent use:

if (e is not Customer c) { throw ... } // if this branch throws or returns...
var n = c.FirstName; // ... c is definitely assigned here

Target-typed new expressions

"Target typing" is a term we use for when an expression gets its type from the context of where it’s being used. For instance null and lambda expressions are always target typed.

new expressions in C# have always required a type to be specified (except for implicitly typed array expressions). In C# 9.0 you can leave out the type if there’s a clear type that the expression is being assigned to.

Point p = new (3, 5);

This is particularly nice when you have a lot of repetition, such as in an array or object initializer:

Point[] ps = { new (1, 2), new (5, 2), new (5, -3), new (1, -3) }; 

Covariant returns

It’s sometimes useful to express that a method override in a derived class has a more specific return type than the declaration in the base type. C# 9.0 allows that:

abstract class Animal
{
    public abstract Food GetFood();
    ...
}
class Tiger : Animal
{
    public override Meat GetFood() => ...;
}

And much more…

The best place to check out the full set of C# 9.0 features is the "What’s new in C# 9.0" docs page.

72 comments

Discussion is closed. Login to edit/delete existing comments.

  • Nathanael Marchand 0

    “This means that properties must have an init or get accessor to be changed in a with-expression.”
    You mean a set accessor no ?

    • Mads TorgersenMicrosoft employee 0

      Oops! Fixed. Thanks!!!

  • MgSam 0

    In other words, if you use an init; setter and set it to Mads Nielsen you’ll be wishing you could go back in time to set it to Mads Torgersen. 🙂

    • Mads TorgersenMicrosoft employee 0

      For the record: I did change my last name from Nielsen to Torgersen in 1995. I don’t plan on changing it back. 🙂

      • Shreejit Gangadharan 0

        Ah I thought it was a reference to the series “Dark” but of course, they’re based on real German names 🙂

  • Plakhotnyi Andrii 0

    What are the ways to validate constructor parameters in records?

    • Vincent Croquette 0

      I have exactly the same question, for individual property initialization logic, one way to do it is to move the validation code from the constructor to the init setter but that does not work as soon as multiple parameters are involved in the validation.

      Another question… if you have:

      class Person
      {
          public string FirstName { get; }
      
          public string? LastName { get; }
      
          public Person(string firstName, string? lastName = null)
          {
              FirstName = firstName;
              LastName = lastName;
          }
      }
      

      Converting to a record is easy but how to make the lastName parameter optional?

      • Rolf van Kuijen 0

        To a record just rename class to record, for optional you can just do this:

        public record Person (string FirstName, string? LastName = null);
        

        If you mean that you want two constructor options then either define the constructors, though you are allowed something like this:

        public record Person(string FirstName, string? LastName = null)
        {
            public double Money {get; }
            public Person(string firstName, double money): this(firstName, "")
            {
                this.Money = money;
            }
        }

        • Vincent Croquette 0

          @Rolf Doh! I hadn’t even tried that, works, thanks!

    • Rolf van Kuijen 0

      If you use positional records then it assumes you don’t really need to.
      If you use records then you just make a constructor and do your validation there, or if you want static Create method use that.

    • Mads TorgersenMicrosoft employee 0

      Validation has been on our minds, and there are two things we are looking at for C# 10:

      1) Primary constructor bodies: Provide some syntax for specifying a code body for the primary constructor, e.g.

      Person 
      { 
          if (FirstName.Length + LastName.Length > 32) 
          {
              throw new InvalidOperationException("Full name too long!"); 
          }
      }

      2) Final initializers: What if you use object initializers? How do you ensure that the object state after the object initializer is valid? Maybe something like this:

      init 
      { 
          if (FirstName.Length + LastName.Length > 32) 
          {
              throw new InvalidOperationException("Full name too long!"); 
          }
      }

      Where this code is guaranteed to run at the end of the object creation expression (after object initializer) for that type.

      • Paulo Pinto 0

        Maybe a better approach would be to bring Design by Contract back, and make it available in all .NET variants instead of the Enterprise only of the previous version.

      • William Kempf 0

        Primary constructor bodies won’t cut it, and including it could lead to confusion, because the constructor is bypassed when you use “with”. Today you have to write a lot of ugly code to validate records.

        public record Person
        {
           public Person(string firstName, string lastName)
              => (FirstName, LastName) = (firstName, lastName);
           public void Deconstruct(out string firstName, out string lastName)
              => (firstName, lastName) = (FirstName, LastName);
           private string firstName;
           public string FirstName
           {
              get { return firstName; }
              init { firstName = NotNull(value, nameof(FirstName); }
           }
           private string lastName;
           public string LastName
           {
              get { return lastName; }
              init { lastName = NotNull(value, nameof(LastName); }
           }
        }

        Count me in on the “Final initializers” vote.

        Another consideration… these represent “value types” and many value types not only support equality, but ordinality as well. There should be a mechanism for implementing the IComparable interface as well. As it stands, there are a lot of “value types” that you still have to write the majority of the implementation out by hand, basically rendering “record” a niche concept.

        • Jacek Ostrowski 0

          All this ugly validation code doesn’t help when you have to assure that required properties were set at all. It’s also hard to implement validation checks taking several properties into account.

          I also like the init finalizer option.

  • Pakpoom Suksomrat 0
    person.LastName = "Torgersen"; // ERROR!

    This is not quite clear to me whether it is compile-time or runtime.

    • Vincent Croquette 0

      This code raises a compile time error

  • Ilia Mazurenko 0

    From now we have structs, classes and records, three types of objects with different behavior.

    I think, the development of the c# language has turned to the wrong direction.

    • Max Mustermueller 0

      Yup. For me its still not clear what’s the benefit (in terms of PERFORMANCE) about records. Are they faster than structs? Using less memory? Also speaking about less boiler plate. It’s fine that properties are now automatically created. But why is that not done right? For example why does it not also raise a property changed notification when it inherits INotifyPropertyChanged?

      For these we cannot use records and still have to write all of it. So records are only helping a very few people. Not sure if that was worth it to be added, leaving more questions and existing problems.

      • Ben Morris 0

        records are a specialization of class. They are no different from any other class, performance-wise.

        Re INotifyPropertyChanged, records are mainly intended to support simple, immutable types in C# — which obviates the need for INPC support. Still, if that’s important to you, there’s already an excellent solution in IL weavers like FoDy and PostSharp.

    • Soroush Falahati 0

      It’s a godsend for me personally.
      I have many open-source wrapper libraries and many times I have to write hundreds of lines of code for a simple declaration of an entity including all properties, constructor, equality methods, and GetHashCode. Not to mention the fact that I have to actively maintain these methods after a simple change to the properties.
      This also essentially makes structs irrelevant except in specific Pinvoke cases. So I think of it as a better, actually functional struct. It is also great for models and DTOs; places in which going with a class can open multiple doors for error with equality checks. In theory, anywhere that you have a class that holds information and is immutable, you should seriously consider making it a record. Hence why there is no support for `INotifyPropertyChanged`.
      So I am not against the new record syntax (let’s be honest it’s just a syntax sugar and nothing fundamental) especially since you don’t have to use it. However, I do understand the point you are trying to make and how it seems that these additions make the language more complicated and confusing. Especially since both class and struct had fundamental meanings but record does not. Honestly, I think the syntax should have been record class instead of record to clarify this fact that this entity is actually a class but with additional features provided by the record syntax sugar.

      • Mads TorgersenMicrosoft employee 0

        Honestly, I think the syntax should have been record class instead of record to clarify this fact that this entity is actually a class but with additional features provided by the record syntax sugar.

        There was some debate about this. The desire for brevity won out here. In C# 10 we are likely to make both record class and record struct available, and treat record as a shorthand for record class. We have been designing what record structs mean, and they would occasionally be useful.

    • Ian Marteens 0

      I also think so: it’s starting to move in circles and accumulating entropy. A couple of weeks ago I finally found a use case for pattern matching in my code. It was midnight. The next day, after waking up, but before the coffee, I found a better way to do it without pattern matching.

      • Ben Morris 0

        Pattern matching continues the move towards concise, functional-style programming support in C#. In particular, the ability to create complex expressions is dramatically improved with the latest pattern matching features. If that’s not your style, you don’t have to use it — but don’t assume that other C# developers don’t find it useful.

        • Ian Marteens 0

          It’s more load on an already overloaded language. Why just don’t design a clean language from scratch for this? Good OOP is bad FP and viceversa. In my example, I’d just missed an abstraction. The “pure” OOP code was simpler than the hybrid one. My two-cents, of course.

          • Danstur 0

            So you also want to get rid of LINQ? LINQ is after all an incarnation of a purely functional concept (it’s nothing but the bind monad). Async/Await? Also a functional concept.

            There’s always resistance when a new concept is introduced, but few people would deny that both of the above concepts made C# a much better language than if it had stuck with “pure OOP” (actually “pure OOP” wouldn’t have classes either if you go back to the roots, OOP in its original form is about message passing something very much hidden in modern class based OOP approaches).

            There are many very useful things you can do with pattern matching (although pattern matching in C# is still rather limited), you just have to learn about them and get used to it. Just as people had to do with LINQ and async/await.

    • anonymous 0

      this comment has been deleted.

    • Andrew Stephens 0

      Agreed. I’ve been saying this for the last couple of versions. While there have been some useful new features along the way, a lot of it seems to be change for change’s sake, and some of the new “time saving” syntax changes introduced in recent versions are truly horrible to read, and not immediately intuitive. Give me readable code any day, even if it means having to write an extra line or two.

      • James Lonero 1

        I agree. C# is getting slowly becoming more like Javascript with the too frequent updates and changes. Can we keep C# on a professional level? It already was better than C++, much less confusing, and safer to use.

        • Ben Morris 0

          Plenty of “professional” developers, including me, find the new features extremely useful.

          On a general note, I really don’t understand these kinds of comments. Nobody is forcing you to use the new language features.

          • William Kempf 1

            I am absolutely not in the “it’s too complex, stop changing it” crowd, but “nobody is forcing you to use it” is simply wrong. Very few of us work individually and even fewer don’t rely on reading code to learn. So, as long as someone is using a language feature, everyone is basically forced to “use” it.

          • anonymous 0

            This comment has been deleted.

          • Oleg Mikhailov 1

            I’m not sure that developers voting for new features on GitHub are really developers. Usually, percent of beginners in the open source communities is very high and it seems like C# team does not realise that 70-90% of the people voting for copying every controversional “feature” from Kotlin or F# are students who does not really work with C#. Let’s be honest, almost nobody uses F# for real world projects, it is a playground and the language for education intended to show students that functional programming exists and that declarative languages had some market share 50 years ago. After the classes students come to GitHub, propose and upvote new features similar to what they saw in the classes, C# team poorly implements them, and finally we receive the notification about new blog post here saying that new bunch of weird staff will be added to the language in the next release, it is already planned and not discussable. This is how C# 8 and C# 9 were designed, except that C# 8 was more about copying the worst things from Kotlin, such as forced non-nullable reference types. The most disappointing thing is that massive negative feedback about controversial features or unreadable syntaxis is being ignored, and once the feature comes from GitHub it never being cancelled. And one more thing: C# team works as if their KPI was bound to the number of features they implement per year, but this is wrong because C# is already mature language. Moreover it is the best language in the whole human history. Well, it was before C#8. It needs to be maintained and polished, not bloated and contaminated.

    • Ben Morris 0

      records are a specialization of class; they don’t have “different behavior”, just some automatic behavior which helps to implement a pretty common POCO pattern.

    • Emmanuel Adebiyi 0

      You’re free to ignore any of the language features that you dislike. Anyone who writes lots of DTOs will most likely appreciate records

  • James Chaldecott 0

    Hi Mads. Great stuff!

    Is it just me, or is there no documentation of records on docs.microsoft.com yet (except in the What’s New in C# 9.0 article)? I was expecting something in the C# Programming Guide.

    I’m looking for more details on how records work, as between this post and your .Net Conf talk yesterday I’m unclear as to whether the default behaviour for properties declared with the positional syntax are immutable or not. Also it would be good to have docs on what methods to override to change equality behaviour (e.g. case-insensitive strings), etc.

    • Martin Malar 0

      I’m unclear as to whether the default behaviour for properties declared with the positional syntax are immutable or not.

      It is mentioned here in the blog post under the Positional records section:

      public record Person(string FirstName, string LastName);

      This declares the public init-only auto-properties and the constructor and the deconstructor

      • James Chaldecott 0

        Sure, but in the .NETConf talk I thought they said that properties declared like this would have setters. I’ve since checked, and this blog is correct: the properties are init-only.

        My point is that there doesn’t seem to be any actual official documentation that I can check, just a series of blog posts. If I fancy digging that far I could find the language proposal, but in my mind a proposal isn’t really documentation of what has shipped.

  • Martin Richards 0

    These features are great, though I think when I first learned to code ~20 years ago all this extra syntax would have been overwhelming.

  • Zehev Spitz 0

    “Currently, a type pattern needs to declare an identifier” should probably be “Previously, a type pattern needed to declare an identifier”.

    • Mads TorgersenMicrosoft employee 0

      Haha, good catch! It was “currently” when I wrote it. 😉

      Fixed!

  • Jefferson Motta 0

    WONDERFULL!

    HOW DO I USE C# 9 WITH ASP.NET AJAX?

    Best,

    Jefferson2020

    • Emmanuel Adebiyi 0

      You can just change LangVersion to 9.0 from your .csproj file. I believe it should work with any project >= .NET Standard 2.0

  • Muneeb Baig 0

    Thanks for the details and explanation… Looks like a merger of ES6 JavaScript and C# LOL. I must say

    with

    is a new

    ...

    Spread Operator.

    Really excited to try out these features though!

    • Hamid R 0

      Scaffolding makes ASP.NET Core app development easier and faster by generating boilerplate code for common scenarios.
      درمان واریس

Feedback usabilla icon