George Kosmidis

Microsoft MVP | Speaks of Azure, AI & .NET | Founder of Munich .NET
Building tomorrow @
slalom
slalom

C# 9.0

by George Kosmidis / Published 4 years ago

It may be that .NET 5, the one and only .NET that will clear the confusion and lead the way for the next years was probably the biggest(?) announcement of Microsoft Build 2020, but there were numerous other equally important; from the general availability of the Blazor WebAssembly, the Azure Static Web Apps and all the projects related to IoT and Artificial Intelligence, all the way to .NET MAUI (short for Multi-platform App UI), Visual Studio Codespaces,  Entity Framework Core 5, Project Tye, Azure Quantum and the multiple new features and capabilities of Azure Cosmos DB.

Although there were many more interesting things, C# 9 was left out intentionally because in this post we will deal with some of its exciting new features!

init accessor

Up until C# 9, in order to use the object initializer syntax the properties of that object had to be mutable, which means they could change anywhere in the code even after object initialization. In other words, there was no way to use object initilizer on immutable properties, or even better a property had to be publicly accessible to use object initializer:

//In C# 8, a mutable object like the following allowed object initializer syntax
public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

//A new instance of "Person" using object initializer syntax
new Person
{
    FirstName = "George",
    LastName = "Kosmidis"
}

//An attempt to use object initializer to create an instance in the following object would result in error
public class Person
{
    public string FirstName { get; private set; }
    public string LastName { get; }
}
// CS0272 The property or indexer 'Person.FirstName' cannot be used in this context because the set accessor is inaccessible
// CS0200 Property or indexer 'Person.LastName' cannot be assigned to -- it is read only

The init accessor comes to solve this problem, by allowing the object initializer syntax but no field mutation after initialization:

//In C#9 you can use the "init" accessor that honours immutability 
public class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

//A new instance can be created using object initializer syntax
var me = new Person
{
    FirstName = "George",
    LastName = "Kosmidis"
}

//But a change in a property value will throw an error
me.FirstName = "NewName";
// CS0200 Property or indexer 'Person.FirstName' cannot be assigned to -- it is read only

Immutability with readonly

The public string FirstName { get; private set; } in the example is not really immutable because it is allowing changes within the object after object initialization. In C# 8, a truly immutable object that wouldn’t of course allow object initializer syntax would be like this:

public class Person
{
        private readonly string firstName;
        private readonly string lastName;
        
        public string FirstName => firstName;
        public string LastName => lastName

        public Person(string firstName, string lastName)
        {
            this.firstName = (firstName ?? throw new ArgumentNullException(nameof(FirstName)));
            this.lastName = (lastName?? throw new ArgumentNullException(nameof(LastName)));;
        }
}

// -OR- since C# 6 using "readonly automatically implemented properties"
public class Person
{
    public string FirstName { get; }
    public string LastName { get; }

    public Person(string firstName, string lastName)
    {
        this.FirstName = (firstName ?? throw new ArgumentNullException(nameof(FirstName)));
        this.LastName = (lastName?? throw new ArgumentNullException(nameof(LastName)));;
    }
}

The init accessors can be used in similar scenarios where readonly fields are necessary, because init accessors can only be called during initialization and thus they are allowed to mutate readonly fields:

public class Person
{
    private readonly string firstName;
    private readonly string lastName;
    
    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

In a nutshell, records are a new lightweight immutable type that affects the immutability of an entire object -not just its properties-, thus making it behave more like a value (it should be seen more as data and less as object):

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

The data keyword on the class declaration marks it as a record. Although they can have methods, properties or even operations they will still allow structural equality comparisons but not encapsulated state mutation. Instead, on each state change, new records should be created that will reflect this change, and C# 9 has a easy way to do this with the with expression!

with expressions

Immutable objects do not represent state over time (they cannot change!) but state at a specific point in time; a common practice to follow changes over time with immutable objects is to create a copy of the initial object changing only the properties that indeed changed, a process called non-destructive mutation.
The with expression comes to help this coding style, following object initializer syntax:

//An immutable object
public data class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

//A new instance using object initializer since the "init" accessor was used
var me = new Person
{
    FirstName = "George",
    LastName = "Kosmidis"
}

//I didn't change to my brother over time, but it serves as a sample!
var myBrother = me with { FirstName = "Chris" };

Altering with behavior with a custom constructor

Under the hoods a protected constructor is implicitly defined and used by with to copy property values from the original object, applying at the same time any changes defined. If the default with behavior is not good enough, a “copy-constructor” can be explicitly defined and this one will be called instead:

//A record with an explicitly defined protected "copy-construtor" to be used with "with"
public data class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
    
    protected Person(Person original) {
        this.FirstName = (original.FirstName ?? throw new ArgumentNullException(nameof(FirstName)));
        this.LastName = (original.LastName ?? throw new ArgumentNullException(nameof(LastName)));
    }
}

//Create a new instance of "Person"
var me = new Person
{
    FirstName = "George", 
    LastName = "Kosmidis"
}

//Try to create my sister with null as FirstName, because I don't have a sister!
var mySister = me with { FirstName = null };
//throws System.ArgumentNullException: 'Value cannot be null. (Parameter 'FirstName')'

Syntactic sugar for Records

Since records are intended to be immutable, the defaults of a simple member declaration has changed:

//For records only, the following declarion doesn't mean the members are private
public data class Person { 
    string FirstName; 
    string LastName; 
}

//On the contrary, it is equal to this:
public data class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

Going a step further, records are implicitly defining positional constructor and destructor:

//The following
public data class Person(string FirstName, string LastName);

//is equivalent with this:
public data class Person 
{ 
    string FirstName; 
    string LastName; 
    
    public Person(string firstName, string lastName) 
        => (FirstName, LastName) = (firstName, lastName);
    
    public void Deconstruct(out string firstName, out string lastName) 
        => (firstName, lastName) = (FirstName, LastName);
}

//or by also expanding the data members, to this:
public data class 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);
}

Altering the default behavior of the positional constructor/destructor is possible by explicitly defining new ones.

Improved pattern matching

Pattern matching, initially added in C# 7 and improved in C# 8, is a way to test that a value has a certain shape, and extract information from the value when it has the matching shape. Pattern matching play a significant role in producing cleaner code for algorithms that are frequently needed today; for example extracting and consuming information from diverse resources that don’t share a common model and whose model isn’t even part of the original system.

Read a great tutorial about pattern matching in Microsoft Docs.

C# 9 added several new kinds of pattern, for simple, relational and logical patterns. Let’s expand our record above to include age and use it as a example:

public data class Person { 
    string FirstName; 
    string LastName; 
    int YearsWorking; 
}

Simple type patterns

Currently, a type pattern needs to declare an identifier when the type matches even if that identifier is a discard using _. Well, not any more:

//From this:
var experienceLevel = 
    person switch
    {
        Person p when p.Age <= 1=> ExperienceLevel.Low,
        Person p when p.Age <= 5 => ExperienceLevel.Medium,
        Person _ => ExperienceLevel.High
    };

  
//To this:
var experienceLevel = 
    person switch
    {
        Person p when p.Age <= 1 => ExperienceLevel.Low,
        Person p when p.Age <= 5 => ExperienceLevel.Medium,
        Person => High //the underscore is gone
    };

Relational patterns

Patterns that correspond to the relational operators (<, >, <=,>=) are introduced that will contribute to cleaner more readable code. Check how to example above is transformed:

//From this:
var experienceLevel = 
    person switch
    {
        Person p when p.Age <= 1 => ExperienceLevel.Low,
        Person p when p.Age <= 5 => ExperienceLevel.Medium,
        Person _ => ExperienceLevel.High
    };

  
//To this:
var experienceLevel = 
  Person p when p.YearsWorking switch
  {
      <= 1 => ExperienceLevel.Low,
      <= 5 => ExperienceLevel.Medium,
      _ => ExperienceLevel.High,
  };

Logical patterns

Finally, logical patterns (and, or, not) are introduced that combine other patterns. They are spelled out as words to avoid confusion with operators that are used within a pattern:

//From this:
var experienceLevel = 
    person switch
    {
        Person p when p.Age <= 1 => ExperienceLevel.Low,
        Person p when p.Age <= 5 => ExperienceLevel.Medium,
        Person _ => ExperienceLevel.High
    };

  
//To this:
var experienceLevel = 
  Person p when p.YearsWorking switch
  {
      <= 1 => ExperienceLevel.Low,
      > 1 and <= 5 => ExperienceLevel.Medium,
      _ => ExperienceLevel.High,
  };

There are more…!

Here is a list of all the features coming to C# 9!

I didn’t write this list on my own, I took it from the Language Feature Status!

This page is open source. Noticed a typo? Or something unclear?
Edit Page Create Issue Discuss
Microsoft MVP - George Kosmidis
Azure Architecture Icons - SVGs, PNGs and draw.io libraries