C# 9.0 features : a developer handbook

Binod Mahto
5 min readOct 12, 2021

I was preparing a presentation for my team to introduce them with C# 9.0 features and realized it could be worth sharing here as well. Also felt it could be like a reference hand book for a developer to know about C# 9.0. So lets begin.

C# 9.0 has been introduced with .NET 5 target framework from Microsoft. Here are the features available with C# 9.0.

  1. record type: a reference type that provides built-in functionality for encapsulating data and it provides capability of value-based equality, means if all the fields are having same value of record instances then those instance will be equal. syntax to create record type:
    public record Person(string FirstName, string LastName); 

//or
public record Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Person(string fName, string lName)
{
FirstName = fName;
LastName = lName;
}
}
//Initialization
var person1 = new Person("Binod", "Mahto");
//orvar person2 = new Person { FirstName = "Binod", LastName = "Mahto"};//orPerson person3 = new ("Binod", "Mahto");//and
person1.Equals(person2) will result true.

Both has same meaning. To mark the Person class immutable change the setters of the data members to init. i.e.

public record Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public Person(string fName, string lName)
{
FirstName = fName;
LastName = lName;
}
}

In above code Data members (properties) are equal to a class readonly field which can be be initialized from the constructor only. But for record type these members can be initialized for base class constructor too (you guess it correct, record type supports inheritance) which is not possible for class. i.e.

public record Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public Person(){}
}
public record Artist : Person
{
public int Age { get; set; }
public Artist(string fName, string lName, int age)
{
this.FirstName = fName;
this.LastName = lName;
this.Age = age;
}
}

How to instantiate the record type and equality comaprison:

var person = new Person("Binod", "Mahto");var person = new Person { FirstName = "Binod", LastName = "Mahto"};

2. Pattern matching enhancements: enhancement related to pattern-matching are very handful & useful.

i. match a variable is a type

      var a = 2;
if(a is int)
Console.WriteLine(a);

ii. Conjunctive (and), Disjunctive (or), Negated(not) Patterns:

     var c = 'a’;
if(c is >= 'a' and <= 'z' or >= 'A' and <= 'Z’)
Console.WriteLine(c);

iii. Parenthesized patterns: enforce or emphasize the precedence of pattern combinations

var c = 'a'
if(c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or ‘,’)
Console.WriteLine(c);

iv. Relational patterns: permit the programmer to express that an input value must satisfy a relational constraint when compared to a constant value.

Note: Relational patterns support the relational operators <, <=, >, and >= on all of the built-in types sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, nint, and nuint.

        public static string LifeStageAtAge(int age) => age switch
{
< 0 => "Prenatal",
< 2 => "Infant",
< 4 => "Toddler",
< 6 => "EarlyChild",
< 12 => "MiddleChild",
< 20 => "Adolescent",
< 40 => "EarlyAdult",
< 65 => "MiddleAdult",
_ => "LateAdult",
};

Here is the complete example to try it out:

    public static class Extension
{
public static string LifeStageAtAge(this int age) => age switch
{
< 0 => "Prenatal",
< 2 => "Infant",
< 4 => "Toddler",
< 6 => "EarlyChild",
< 12 => "MiddleChild",
< 20 => "Adolescent",
< 40 => "EarlyAdult",
< 65 => "MiddleAdult",
_ => "LateAdult",
};
public static bool IsLetter(this char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z'; public static bool IsLetterOrSeparator(this char c) =>
c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';
}

3. Extending Partial Methods: Earlier Partial method restrictions have been removed as enhancement which were:
i. Must have a void return type.
ii. Cannot have out parameters.
iii. Cannot have any accessibility (implicitly private).

So you can now apply all above three with partial methods. i.e.:

     partial class Myclass
{
partial void Method1();
public partial void Method2();
internal partial void Method3(out int i);
private partial int Method4();
}

4. Module Initializers: A method can be designated as a module initializer by decorating it with a [ModuleInitializer] attribute and this method will be called first in your program.
i.e.

    using System.Runtime.CompilerServices;  

class MyClass
{
static void Main1(string[] args)
{
Console.WriteLine("Main Method");
}
[ModuleInitializer]
internal static void MyMethod()
{
Console.WriteLine("Initializer");
}
}
//this code will output as:
Initializer
Main Method

5. Target Typed new expression: omit the type in a new expression when the created object’s type is already known. “Not sure even this is needed when you already have ‘var’ in place. i.e.

List<string> stringList = new();    List<Person> persons = new() { new() { FirstName = "Binod", LastName = "Mahto" } };

Or even same can be applied to method as well. Honestly this feature didn’t excite me at all.

        void MyMethod(List<string> listStr)
{
//Doing something
}
//You can call this method like below which pass an empty List<string>:
MyMethod(new());

6. Target-Typed Conditional Expression: Local function declarations are now permitted to have attributes. Parameters and type parameters on local functions are also allowed to have attributes. I love this features.

Lets see the example with debug code which I want to add in my program but restrict to get executed only in debug mode. In below code example, even though I’m calling the PrintOnDebug() method in Main method but it gets called only when I run the program in debug mode.

        using System.Diagnostics;    
class Program
{
static void Main(string[] args)
{
PrintOnDebug();
Console.WriteLine("Main");
}
[Conditional("DEBUG")]
static void PrintOnDebug()
{
Console.WriteLine("You are debugging");
}
}

7. Extension GetEnumerator support for foreach loops. : C# 9.0 enables you to use the foreach with IEnumerator types as well but you have to add the below extension class and method to your code first.

    public static class Extensions
{
public static IEnumerator<T> GetEnumerator<T>(this IEnumerator<T> enumerator) => enumerator;
}

once we add above extension class and method we can use the forach with IEnumerator types. i.e

var countryEnumerator = (IEnumerator<string>)new List<string> { "India", "France", "Canada", "Japan" };
foreach (var country in countryEnumerator)
{
Console.WriteLine($"{country} is a beautiful country");
}

8. Covariant returns: permit the override of a method to declare a more derived return type than the method it overrides, and similarly to permit the override of a read-only property to declare a more derived type. i.e.

    public class Employee { }
public class Developer : Employee
{
public string Team { get; set; }
}
public class Employer
{
public virtual Employee GetTeamEmployee()
{
return new Employee();
}
}
public class Kalinga : Employer
{
public override Developer GetTeamEmployee()
{
return new Developer();
}
}

C# 9.0 allows you to use the child type for overridden methods of child class.

That’s all. Hope you enjoyed reading it. Happy Coding thank you.

--

--

Binod Mahto

Solution Architect & Full Stack Developer. Passionate about Software designing & development and Learning Technologies and Love to share what I learn.