C# 9.0 New Features
access_time 7 months ago languageEnglish

C# 9.0 New Features

visibility 564 comment 0

.NET 5.0 release candidate 1 (rc.1) was published on 2020-09-14, which marks another big step towards the official .NET 5.0 release. As part of 5.0, C# 9.0 will be released with a bunch of new features. This article summarizes some of the new features with examples.

Install .NET 5.0 SDK

Download .NET 5.0 SDK from this link: SDK 5.0.100-rc.1.

Verify the installation using the following command line:

dotnet --list-sdks
2.0.0 [C:\Program Files\dotnet\sdk]
2.1.202 [C:\Program Files\dotnet\sdk]
2.1.400 [C:\Program Files\dotnet\sdk]
3.0.100 [C:\Program Files\dotnet\sdk]
3.1.101 [C:\Program Files\dotnet\sdk]
3.1.200 [C:\Program Files\dotnet\sdk]
3.1.400 [C:\Program Files\dotnet\sdk]
3.1.402 [C:\Program Files\dotnet\sdk]
5.0.100-rc.1.20452.10 [C:\Program Files\dotnet\sdk]
Check the current version in use:

dotnet --version

Now let's start to create a new project to explore these C# new features.

Create a dotnet console project

Use the following commands to create a Console application using .NET SDK 5.0 SDK.

mkdir csharp9
cd csharp9
dotnet new console

There are two files created:

Program.cs  csharp9.csproj

Run the following command to compile and execute the application:

dotnet build
dotnet run

The output looks like this:

Hello World!

Now open this project in Visual Studio Code to code easily:

code .

The project looks like the following screenshot in VS Code:

Now let's start to explore these new features in C# 9.0.

Init-only setters

Before version 9.0, C# provides very easy syntax to create properties. 

The following code snippet creates a class with two properties:

public class MyClass
	public MyClass(string a, int b) => (A, B) = (a, b);

	public string A { get; set; }
	public int B { get; private set; }

The code is already very simple with getters and setters. To use this class, we can change the main function in Program.cs to the following:

class Program
	static void Main(string[] args)
		var myClass = new MyClass("Hello, C# ", 9);

This program outputs the following:

Hello, C# 9

From very beginning when C# was created, most of types are created as reference types. If you favor programming with immutable, the setters in the above example can be a problem as the value of property A can be changed after initialization:

myClass.A = "Hi, C# ";

The new init-only setter comes to rescue and you can define the class with init settings:

public class MyClass
	public string A { get; init; }
	public int B { get; init; }

And then the class can be instantiated:

var myClass = new MyClass { A = "Hello, C# ", B = 9 };

A compiler error will be raised if assignment is done to property A once the object is initialized:

error CS8852: Init-only property or indexer 'MyClass.A' can only be assigned insigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.

Init accessors and read-only fields

This new init accessor can also be allowed to mutate the read-only fields of enclosing class:

public class MyClass2
	private readonly string a;
	private readonly int b;

	public string A { get => a; init => a = value ?? throw new ArgumentNullException(nameof(A)); }
	public int B { get => b; init => b = value; }

You can mutate the values of A and B in object initializer:

var myClass2 = new MyClass { A = "Hello, C# ", B = 9 };

Record types

From C# 9.0, record types are introduced. It is a reference type that provides synthesized methods to provide value semantics for equality. Records are immutable by default. Immutable variables are commonly used in distributed big data computing frameworks. It can help reduce errors when working with data concurrently. 

Define a record type

Record type can be defined easily using keyword record. In earlier preview versions, the keyword was data.

public record Customer
	public string LastName { get; }
	public string FirstName { get; }

	public Customer(string first, string last) => (FirstName, LastName) = (first, last);

You can also add init setters but it is not mandatory as the compiler will do that by default. 

To use the defined record type, you can just easily initialize the object as you would do with normal reference types:

var cust = new Customer(first: "Raymond", last: "Tang");

Roslyn compiler automatically synthesizes several other methods:

  • Methods for value-based equality comparisons
  • Override for GetHashCode()
  • Copy and Clone members
  • PrintMembers and ToString()
  • Deconstruct method

Positional records

There is a new very concise syntax to define a record type called positional records. The previous Customer record can be defined using the following code:

public record Customer(string FirstName, string LastName);
var cust = new Customer("Raymond", "Tang");

Deconstruct method

With positional record types, data can be easily deconstructed via the Deconstruct method:

var cust = new Customer("Raymond", "Tang");
var (first, last) = cust;
Console.WriteLine($"{first} {last}");

In the above code snippet, data is deconstructed to two variables named first and last. A with-expression instructs the compiler to create a copy of a record, but with specified properties modified:

var cust2 = cust with { FirstName = "Ray" };

The above code snippet creates another object by copying from cust variable with FirstName changed to 'Ray'. Object initialization syntax is used together with with-expression to change values. 


Since record type itself is a reference type, inheritance is also allowed.

public record RetailCustomer(string FirstName, string LastName, int RetailAccountsCount) : Customer(FirstName, LastName);

The above code snippet creates a new record type named RetailCustomer with one more property named RetailAccountsCount.


Record types comparison are based on values:

var cust1 = new Customer("Raymond", "Tang");
var cust2 = cust1 with { FirstName = "Ray" };
var cust3 = cust2 with { FirstName = "Raymond" };


The output will be 'False' and 'True'.

Top-level statements

I previously always like other scripting languages' easiness without the need to create a main entry function. Now from C# 9.0, we can also do this easily using top-level statements. The typical use cases are Jupyter Notebooks and Azure Functions. In one project, only one file can have top-level statements. 

The 'Hello, world!' example can now be changed to just one line of code:

System.Console.WriteLine("Hello C# 9!");

Pattern matching improvements

As mentioned in the official documentation, there are several pattern matching improvements:

  • Type patterns match a variable is a type
  • Parenthesized patterns enforce or emphasize the precedence of pattern combinations
  • Conjunctive and patterns require both patterns to match
  • Disjunctive or patterns require either pattern to match
  • Negated not patterns require that a pattern doesn’t match
  • Relational patterns require the input be less than, greater than, less than or equal, or greater than or equal to a given constant.

The following are some examples:

using static System.Console;

WriteLine("Hellow, C# 9!");

static bool IsLetter(char c) =>
    c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

static bool IsLetterOrSeparator(char c) =>
    c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';


dynamic text = null;

if (text is null)
    WriteLine("text is null");

if (text == null)
    WriteLine("text is null");

One of the my favorites is 'is not' syntax:

using static System.Console;

var cust = new Customer("Raymond", "Tang");
if (cust is Customer)

if (cust is not RetailCustomer)
record Customer(string FristName, string LastName);
record RetailCustomer(string FristName, string LastName, int RetailAccountsCount) : Customer(FristName, LastName);

The output is 'Yes' for both if statements. 

Covariant returns

C# 9.0 now allows an override method to returns a type that is more specific than the type defined in the base class. This is a feature I've been waiting for quite a long time.

using static System.Console;

WriteLine("Hello, C# 9!");
var panda = new Panda();

abstract class Food
    public string Name { get; set; }
class Bamboo : Food
    public string Color { get; set; }
abstract class Animal
    public abstract Food GetFood();

class Panda : Animal
    public override Bamboo GetFood()
        throw new System.NotImplementedException();

Class Panda overrides GetFood method with returned type as Bamboo which inherits from class Food

Other features

There are many other features that help us to write code very efficiently (let the complier do the work!).

One of the feature is that in a new expression the target type can be omitted if the created object type is already known:

using System.Collections.Generic;
using static System.Console;

// Traditional way
List<int> numbers1 = new List<int>();
// The new concise approach
List<int> numbers2 = new();

It can be combined with init-only properties to initialize values:

Customer cust = new() { FirstName = "Raymond" };
record Customer
    public string LastName { get; init; }
    public string FirstName { get; init; }
Positional record can also be instantiated with new:
Customer cust = new("Raymond", "Tang");
record Customer(string FirstName, string LastName);

Refer to the articles on the reference sections for more details. 


info Last modified by Raymond 7 months ago copyright This page is subject to Site terms.
Like this article?
Share on

Please log in or register to comment.

account_circle Log in person_add Register

Log in with external accounts

Follow Kontext

Get our latest updates on LinkedIn or Twitter.

Want to contribute on Kontext to help others?

Learn more