Skip to content

9.0

C# 9.0, released with .NET 5 in November 2020, introduced several new features aimed at making the language more concise, expressive, and efficient for modern development practices. Here are some of the key features introduced in C# 9.0, along with examples and comparisons to earlier versions of C#:

1. Records

Records are a reference type that provides built-in functionality for encapsulating immutable data. They are ideal for scenarios where the primary purpose of the type is to store data with little to no behavior.

C# 9.0

public record Person(string FirstName, string LastName);

C# < 9.0 Before C# 9.0, creating an immutable class required more boilerplate code, including manually implementing value equality.

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

    public Person(string firstName, string lastName) => (FirstName, LastName) = (firstName, lastName);

    protected bool Equals(Person other) => FirstName == other.FirstName && LastName == other.LastName;

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Person)obj);
    }

    public override int GetHashCode() => HashCode.Combine(FirstName, LastName);
}

2. Init Only Setters

Init only setters provide a way to make individual properties immutable after object construction, enhancing the support for immutable data types in C#.

C# 9.0

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

var person = new Person { FirstName = "John", LastName = "Doe" };
// person.FirstName = "Jane"; // Error: Init-only property or indexer 'Person.FirstName' can only be assigned in an object initializer

C# < 9.0 Previously, to achieve immutability for individual properties, you had to use readonly fields or properties with private setters, which could only be set in the constructor.

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

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

3. Top-level Statements

Top-level statements reduce the boilerplate code required for simple programs, allowing you to write the main body of a program without the need for a Main method.

C# 9.0

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

C# < 9.0 In earlier versions, a Main method was required as an entry point for every console application.

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

4. Pattern Matching Enhancements

C# 9.0 introduced new pattern matching capabilities, such as simple type patterns, relational patterns, and logical patterns, making pattern matching more expressive and powerful.

C# 9.0

public static string GetDescription(object obj) => obj switch
{
    > 0 and < 10 => "Between 0 and 10",
    string s => $"String of length {s.Length}",
    null => "Null",
    _ => "Unknown"
};

C# < 9.0 Pattern matching was less expressive, and certain patterns required more verbose code or were not possible.

public static string GetDescription(object obj)
{
    if (obj is int i && i > 0 && i < 10)
    {
        return "Between 0 and 10";
    }
    else if (obj is string s)
    {
        return $"String of length {s.Length}";
    }
    else if (obj == null)
    {
        return "Null";
    }
    else
    {
        return "Unknown";
    }
}

5. Target-typed new Expressions

C# 9.0 allows the type of the new expression to be inferred from the context, reducing redundancy when the type is already known.

C# 9.0

Person person = new("John", "Doe");

C# < 9.0 Previously, you had to specify the type explicitly for each new expression.

Person person = new Person("John", "Doe");