New features of C# 9.0 (.NET 5.0)

What we'll cover

  1. Top-level programs
  2. Init-only properties
  3. Record
  4. With-expressions
  5. Target-typed new expressions
  6. Inline parameter name hints (bonus)


  1. Visual Studio 2019 version 16.8.2. This version comes with .NET 5.0. Download here.
  2. Change "Target framework" of your project to ".NET 5.0". Right-click on the project, select Properties. Under Application look for Target framework: and select .NET 5.0


On November 10th 2020, Microsoft announced the official released of C# 9.0 with additional features such as record, top-level calls, patten-matching, etc. In this article, I will walk you through the implementations of some of these features, with a bonus - an extra feature added to Visual Studio 2019 version 16.8.2.

Top-level programs

Before the release of this feature, writing a simple Hello World! program in C# required at least 9 lines of code which included using statement, class and Main method declarations as shown below.

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

For language beginners, this is overwhelming compared to writing the same program in Python or JavaScript. In C# 9.0, you can write the same program in two lines! See example below.

using System;
Console.WriteLine("Hello World!");

If you wanted a one-line program, you can replace the using directive with a fully qualified type name like:

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

"Only one file in your application may use top-level statements. If the compiler finds top-level statements in multiple source files, it’s an error. It’s also an error if you combine top-level statements with a declared program entry point method, typically a Main method. In a sense, you can think that one file contains the statements that would normally be in the Main method of a Program class."

Init-only properties

Recall how C# objects are initialized using object initialization syntax. Here is a simple one:

var person = new Person { FirstName = "Victor", LastName = "Akpan" };

This is a flexible and readable syntax for creating an object, and this syntax is especially handy when creating nested objects. Recall also that the new object person above can be modified later in the program because its properties (FirstName and LastName) have to be mutable for object initialization to work. For example:

person.LastName = "Citro"; // VALID

However, if you want to prevent the mutation of object's properties after initialization, say hello to init-only properties. When a property is declared with the init accessor, it means the property can only be assigned a value during object initialization. After initialization, that property is immutable (its value cannot change). For example:

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

var person = new Person { FirstName = "Victor", LastName = "Akpan" }; // VALID
person.LastName  = "Citro"; // ERROR


In a classic C# object-oriented programming, objects have strong identities and they encapsulate properties and fields that evolve, change or mutate overtime during the lifetime of the program. But sometimes you want the whole object to be immutable and behave like a value. Record provides this behavior.

Record types make it easy to create immutable reference types in .NET. Below is an example:

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

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

Behind the scene, record is a class but it is immutable because we cannot modify it once it is created.


The with expression is used to change properties of a record. We understand a record is immutable by default, however, its properties can be mutated if that is intended. See the example below. It's almost the same code above but with the init accessor.

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

var person = new Person { FirstName = "Victor", LastName = "Akpan" }; // VALID
var newPerson = person with { FirstName = "Uyai", LastName = "Adriel" }; // VALID

Target-typed new expression

This C# 9.0 feature is one of my favorites. Before the release of C# 9.0, to create an instance of a class the new keyword followed by the class name was required. For example:

Person person = new Person("Victor", "Akpan");

Now you can use the target type (in this case the Person class on the left) to create an instance on the right without explicitly using the class name. Below is the same declaration with target-typed new expression.

Person person = new ("Victor", "Akpan");

In fact, you can create a collection of persons like this:

Person[] persons = { new ("Victor", "Akpan"), new ("Benson", "George"), new ("Darling", "Maxwell")};

For this to work, it requires the target type on the left of the statement declaration.

var person = new ("Victor", "Akpan"); // ERROR
Person person = new ("Victor", "Akpan"); // VALID

And much more...

For a full set of C# 9.0 features, the best place to look is What's new in C# 9.0 docs page.

Inline parameter name hints

VS inline parameter hints

Visual Studio 2019 version 16.8.2 comes with a new feature (experimental at the time of this writing) called Inline parameter hint. With this feature turned on, you can see the name of parameters in method calls. This hint helps especially when consuming methods from external apis in your application.

To enable parameter hint in Visual Studio 2019 v16.8.2 or above:

  1. Click on "Tools" menu,
  2. Select Options
  3. Expand "Text Editor"
  4. Expand "C#" and select "Advanced"
  5. Scroll down to "Editor Help" and check the option "Display inline parameter name hints (experimental)"
  6. Click OK