What I Like About C# 9

10 minute read

Note: This post was originally posted on the Telerik blog.

I’ve been a software engineer for 20+ years, and as the adage goes, You can’t teach an old dog new tricks. However, if there is one thing I learned in the 20+ years is that I am ALWAYS learning. There are always new technologies coming out, new languages, and new products to solve complex problems. .NET 5 introduced C# 9, which had many new language features. So it was time for me to learn some new tricks and I dove into .NET 5’s C# 9 language additions.

After using these new language features, keywords, and syntax, I noticed that they started to save me keystrokes and time. Since these language additions helped me I wanted to share them with you.

Let’s take a look at some of the new language features.

Records

The new record keyword defines a reference type that provides some built-in functionality for representing data. You might be thinking that this sounds a lot like a class, and you would be correct. It does. However, the intent is to provide smaller and more concise types to represent immutable data. I like to think of them as a type used primarily to transfer data and not have a lot of methods or data manipulation.

More on C# 9 records.

Defining a Record

There are a few different ways to define a record. The simplest form is:

1
public record Person(string FirstName, string LastName);

At first glance, at least for me, that seemed weird. It has a method look and feel. There is even a semicolon at the end. But, the above line creates a Person type with the read/write properties of FirstName and LastName. You can access the Person as follows:

1
2
3
var person = new Person("Joseph", "Guadagno");
Console.WriteLine(person.FirstName); // Outputs Joseph
Console.WriteLine(person.LastName); // Outputs Guadagno

So far, this looks very class-like. Well, it is, except for the declaration. We already saved a bunch of keystrokes. But let’s dig more into it.

Another way to define the Person record is more class-like:

1
2
3
4
5
public record Person
{
    public string FirstName { get; set;}
    public string LastName { get; set;}
}

Creating Records by Position

You can further reduce some typing and remove some boilerplate code using the new positional syntax for records. For example, if you wanted to declare a variable with the class approach and initialize it with data, you would do something like this.

1
var person = new Person { FirstName = "Joseph", LastName="Guadagno"};

With positional syntax, that would look like this.

1
Person person = new ("Joseph", "Guadagno");

That’s 26 fewer characters. Behind the scenes the compiling is creating a lot of the boilerplate code for you. The compiler creates a constructor that matches the position of the record declaration. Since the FirstName property was the first property declared when we defined the method, it assumes that the Joseph value should be the value of the FirstName property. The compiler also generated all the properties as init-only, more on that later, meaning the properties can not get set after initialization making them read only.

Value Equality

One set of built-in functionality that records provide is value equality. When checking to see if two records are equal, it will look at the values of each of the properties and not the reference.

Assuming the definition of.

1
public record Person(string FirstName, string LastName);

When comparing records

1
2
3
4
5
Person person1 = new ("Joseph", "Guadagno");
Person person2 = new ("Joseph", "Guadagno");

Console.WriteLine(person1 == person2); // outputs True
Console.WriteLine(ReferenceEquals(person1, person2)); // outputs False

Since person2 has the same FirstName and LastName of person2 they are equal, although the references are not.

Improved ToString()

Using the record keyword, gets you another built in method. What a deal! An improved ToString method. I really wish this was opt-in standard for classes to.

The ToString method outputs the following format.

<record type name> { <property name> = <value>, <property name> = <value>, ...}

For a record defined as

1
public record Person(string FirstName, string LastName);

and initialized as

1
Person person = new {"Joseph", "Guadagno"};

the ToString method would output a string like

Person { FirstName = Joseph, LastName = Guadagno }

If there is a reference type as one of the properties of the record, the records ToString implementation will output the type name of it.

NOTE Don’t try to use the ToString method to determine the records properties.

Inheriting Records

Records can be inherited the same way classes are except for the following:

  • Records can’t inherit from a class
  • Class can’t inherit from a record
  • When comparing records, the type of record is used as part of the comparison and not just the values.

Copying Records

Copying records is pretty easy. As an added bonus, the syntax makes the code easier to read.

Let’s say I had a Person record defined as.

1
2
3
4
5
6
public record Person
{
    string FirstName { get; set;}
    string LastName { get; set;}
    string HomeState { get; set;}
}

Let’s also say I want to create one Person and make multiple copies and just change a few properties. As if I was to create variables for the whole family. In our case, the LastName and HomeState properties are the same and using records along with the with keyword makes this easier.

1
2
3
4
var me = new Person("Joseph", "Guadagno", "Arizona");
var wife = me with {FirstName = "Deidre"};
var son = me with {FirstName = "Joseph Jr."};
var daughter = me with {FirstName = "Emily"};

Now, the wife, son, and daughter objects have the property of LastName set to Guadagno and HomeState set to Arizona.

Defining Set Once Properties

You can also use the new init keyword to make certain properties settable on initialization only. The init keyword works with properties or indexers in struct, class, or record.

Let’s say with want to define a Person record with FirstName, LastName, and CreateOnDate properties. The CreatedOnDate should not be editable after the record is initialized. We would declare the record like this.

1
2
3
4
5
6
public record Person 
{
    public string FirstName { get; set;}
    public string LastName { get; set;}
    public DateTime CreatedOnDate { get; init;}
}

You see on line 5, we have the keyword init instead of set. This means the CreatedOnDate can only be set when initialized.

1
var person = new Person("Joseph", "Guadagno", DateTime.Now());

After declaring this record, we are limited as to what properties we can change.

1
2
person.FirstName = "Joe"; // valid
person.CreatedOnDate = DateTime.Now(); // You will get a compile error

Line 2, will cause a compilation error because the property CreatedOnDate was set to init-only.

Alternative declaration

You can also declare the setter of a property with a backing field as init-only.

1
2
3
4
5
6
7
8
9
10
11
public class Person
{
    private readonly DateTime _dateOfBirth;
    public DateTime DateOfBirth 
    {
        get => _dateOfBirth;
        init => (value ?? throw new ArgumentNullException(nameof(DateOfBirth)));
    }
    public string FirstName { get; set;}
    public string LastName { get; set;}
}

On line 7, we define the class Person with an init only property DateOfBirth that must be set at initialization or you will get a compile error or runtime exception depending on the implementation.

This is valid (assuming the definition above).

1
var person = new Person{FirstName="Joseph", LastName="Guadagno", DateOfBirth=DateTime.Now()};

This is not (assuming the definition above).

1
var person = new Person{FirstName="Joseph", LastName="Guadagno"};

Based on the above definition, this code sample will throw a runtime exception.

Top Level Statements

I started out this post introducing the notion that C# 9’s language features help you be more productive and reduce keystrokes. Top Level statements is another one of the features. To be honest, you probably won’t use this feature a lot. In fact, you can only have one file in your application that uses this feature. It’s generally helpful for demonstrating some functionality and removing all of the extra ceremony around the application startup. I see myself using it when I am creating presentations.

Let’s take the typical “Hello World” sample.

1
2
3
4
5
6
7
8
9
10
11
12
using System;

namespace CSharp9Features.ConsoleApp
{
    static class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

It’s 12 lines long using the default .NET C# console app template. Now with top level statements, this can reduced to.

1
System.Console.WriteLine("Hello World");

Now “we” reduced the code from 12 lines and 210 characters to 1 line and 40 characters.

Behind the scenes the compiler essentially created the 12 lines and 210 characters for you. But again, C#9 is trying to make things easier for you so why type those lines when the compiler knows that is what you want.

In a more “realistic” example, let’s say for an ASP.NET Core WebAPI project. The typical template would have a Program.cs file that looks something like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Contacts.Api
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
                .ConfigureLogging(logging =>
                {
                    logging.ClearProviders();
                    logging.SetMinimumLevel(LogLevel.Trace);
                });
    }
}

Now with C# 9, I can remove some of the noise and ceremony and have my code just be what my API needs to start.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using Contacts.Api;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

CreateHostBuilder(args).Build().Run();

IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
    .ConfigureLogging(logging =>
    {
        logging.ClearProviders();
        logging.SetMinimumLevel(LogLevel.Trace);
    });

This code now clearly states what the intent of the program.cs is without the extra namespace or Main method.

New Pattern Matching

While pattern matching is not new to C# 9, C# 9 did add a few more patterns.

Logical patterns:

  • and
  • or
  • not

Relations patterns:

  • <
  • >
  • <=
  • >=

These patterns help add readability to code. My favorite addition to this is the not pattern matcher. Now I can take all the instances of

1
if (!person is null)

and make them more readable with

1
if (person is not null)

While this one is more keystrokes, the extra couple of characters makes it more readable to me than the ! operator.

Omitting the type

The compiler is getting smarter. It’s not necessarily getting more intelligent, but getting better at understanding what you are trying to do and, again, reducing the keystrokes. The C# 9 feature of target-typed new expressions demonstrates that the compiler is getting smarter. Now, based on the variable declaration or method signature, you can omit the type in variable declarations or usage.

Here we are declaring a variable _people of type List<Person>

1
private List<Person> _people = new();

You no longer have to initialize the variable of _people with new List<Person>(). The compiler can assume that you want a new List of Person.

The same goes for methods. In the sample below, the method CalculateSalary expects a parameter of type PerformanceRating.

1
2
3
4
public Person CalculateSalary(PerformanceRating rating) 
{  
    // Omitted
}

If I wanted to initialize an new PerformanceRating object for the method without creating a variable, I can now.

1
var person = person.CalculateSalary(new ());

or, I can pass in a new PerformanceRating object with one or more of it’s properties initialized.

1
var person = person.CalculateSalary(new () {Rating ="Rock Star"});

This syntax does take some getting used to. I think in the long it leads to code that is easier to use. However, it might add more fuel to the var vs. typed variable declaration debate. :)

Wrap Up

Wow, that was a lot. C#9 added Record Types, Init Only setters, Top-Level programs, enhancements to pattern matching, and more.

I hope you take some time and play around with these new language features. Doing so will reduce your keystrokes and help your code to be readable in the long run.

Bonus: Coming Soon - C# 10

While not set in stone… As of the writing of this post, .NET 6 preview 5 is planing on adding the following to C# 10.

  • Allow const interpolated strings.
  • Record types can seal ToString().
  • Allow both assignment and declaration in the same deconstruction.
  • Allow AsyncMethodBuilder attribute on methods.

For more, check out What’s new in C# 10.0