Telerik blogs
Industry NewsT2 Light_1200x303

We’ll start with how I’ve been using some of the new features of C# 9 and why I like them. Then we’ll look at an example that uses a Person class and expands on it. At the end, I may even hint at C# 10 features.

I’ve been a software engineer for 20+ years, and as the adage goes, You can’t teach an old dog new tricks. However, if there is one thing I learned in those 20+ years, it’s that I am ALWAYS learning. There are always new technologies coming out, new languages and new products to solve complex problems. .NET 5 introduced C# 9, which had many new language features. So it was time for me to learn some new tricks, and I dove into .NET 5’s C# 9 language additions.

After using these new language features, keywords and syntax, I noticed that they started to save me keystrokes and time. Since these language additions helped me, I wanted to share them with you.

Let’s take a look at some of the new language features.

Records

The new record keyword defines a reference type that provides some built-in functionality for representing data. You might be thinking that this sounds a lot like a class, and you would be correct—it does. However, the intent is to provide smaller and more concise types to represent immutable data. I like to think of them as a type used primarily to transfer data and not have a lot of methods or data manipulation.

More on C# 9 records.

Defining a Record

There are a few different ways to define a record. The simplest form is:

public record Person(string FirstName, string LastName);

At first glance, at least for me, that seemed weird. It has a method look and feel. There is even a semicolon at the end. But, the above line creates a Person type with the read/write properties of FirstName and LastName. You can access the Person as follows:

var person = new Person("Joseph", "Guadagno");
Console.WriteLine(person.FirstName); // Outputs Joseph
Console.WriteLine(person.LastName); // Outputs Guadagno

So far, this looks very class-like. Well, it is, except for the declaration. We already saved a bunch of keystrokes. But let’s dig more into it.

Another way to define the Person record is more class-like:

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

Creating Records by Position

You can further reduce some typing and remove some boilerplate code using the new positional syntax for records. For example, if you wanted to declare a variable with the class approach and initialize it with data, you would do something like this.

var person = new Person { FirstName = "Joseph", LastName="Guadagno"};

With positional syntax, that would look like this.

Person person = new ("Joseph", "Guadagno");

That’s 26 fewer characters. Behind the scenes the compiling is creating a lot of the boilerplate code for you. The compiler creates a constructor that matches the position of the record declaration. Since the FirstName property was the first property declared when we defined the method, it assumes that the Joseph value should be the value of the FirstName property. The compiler also generated all the properties as init-only (more on that later), meaning the properties cannot get set after initialization, making them read-only.

Value Equality

One set of built-in functionality that records provide is value equality. When checking to see if two records are equal, it will look at the values of each of the properties and not the reference.

Assuming the definition of:

public record Person(string FirstName, string LastName);

when comparing records:

Person person1 = new ("Joseph", "Guadagno");
Person person2 = new ("Joseph", "Guadagno");

Console.WriteLine(person1 == person2); // outputs True
Console.WriteLine(ReferenceEquals(person1, person2)); // outputs False

Since person2 has the same FirstName and LastName of person2, they are equal, although the references are not.

Improved ToString()

Using the record keyword gets you another built-in method. What a deal! An improved ToString method. I really wish this was opt-in standard for classes too.

The ToString method outputs the following format.

<record type name> { <property name> = <value>, <property name> = <value>, ...}

For a record defined as:

public record Person(string FirstName, string LastName);

and initialized as:

Person person = new {"Joseph", "Guadagno"};

the ToString method would output a string like:

Person { FirstName = Joseph, LastName = Guadagno }

If there is a reference type as one of the properties of the record, the record’s ToString implementation will output the type name of it.

NOTE: Don’t try to use the ToString method to determine the records properties.

Inheriting Records

Records can be inherited the same way classes are except for the following:

  • Records can’t inherit from a class.
  • Class can’t inherit from a record.
  • When comparing records, the type of record is used as part of the comparison and not just the values.

Copying Records

Copying records is pretty easy. As an added bonus, the syntax makes the code easier to read.

Let’s say we have a Person record defined as:

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

Let’s also say we want to create one Person and make multiple copies and just change a few properties, as if we were creating variables for the whole family. In our case, the LastName and HomeState properties are the same and using records along with the with keyword makes this easier.

var me = new Person("Joseph", "Guadagno", "Arizona");
var wife = me with {FirstName = "Deidre"};
var son = me with {FirstName = "Joseph Jr."};
var daughter = me with {FirstName = "Emily"};

Now, the wife, son and daughter objects have the property of LastName set to Guadagno and HomeState set to Arizona.

Defining Set-Once Properties

You can also use the new init keyword to make certain properties settable on initialization only. The init keyword works with properties or indexers in struct, class or record.

Let’s say with want to define a Person record with FirstName, LastName and CreateOnDate properties. The CreatedOnDate should not be editable after the record is initialized. We would declare the record like this.

public record Person 
{
    public string FirstName { get; set;}
    public string LastName { get; set;}
    public DateTime CreatedOnDate { get; init;}
}

You see on line 5 we have the keyword init instead of set. This means the CreatedOnDate can only be set when initialized.

var person = new Person("Joseph", "Guadagno", DateTime.Now());

After declaring this record, we are limited as to what properties we can change.

person.FirstName = "Joe"; // valid
person.CreatedOnDate = DateTime.Now(); // You will get a compile error

Line 2 will cause a compilation error because the property CreatedOnDate was set to init only.

Alternative Declaration

You can also declare the setter of a property with a backing field as init only.

public class Person
{
    private readonly DateTime _dateOfBirth;
    public DateTime DateOfBirth 
    {
        get => _dateOfBirth;
        init => (value ?? throw new ArgumentNullException(nameof(DateOfBirth)));
    }
    public string FirstName { get; set;}
    public string LastName { get; set;}
}

On line 7, we define the class Person with an init-only property DateOfBirth that must be set at initialization or you will get a compile error or runtime exception depending on the implementation.

This is valid (assuming the definition above):

var person = new Person{FirstName="Joseph", LastName="Guadagno", DateOfBirth=DateTime.Now()};

This is not (assuming the definition above):

var person = new Person{FirstName="Joseph", LastName="Guadagno"};

Based on the above definition, this code sample will throw a runtime exception.

Top-Level Statements

I started out this post introducing the notion that C# 9’s language features help you be more productive and reduce keystrokes. Top-level statements are another one of the features.

To be honest, you probably won’t use this feature a lot. In fact, you can only have one file in your application that uses this feature. It’s generally helpful for demonstrating some functionality and removing all of the extra ceremony around the application startup. I see myself using it when I am creating presentations.

Let’s take the typical “Hello World” sample.

using System;

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

It’s 12 lines long using the default .NET C# console app template. Now with top-level statements, this can be reduced to:

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

Now we reduced the code from 12 lines and 210 characters to 1 line and 40 characters.

Behind the scenes, the compiler essentially created the 12 lines and 210 characters for us. But again, C# 9 is trying to make things easier for you, so why type those lines when the compiler knows that is what you want?

In a more “realistic” example, let’s say for an ASP.NET Core Web API project, the typical template would have a Program.cs file that looks something like this:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Contacts.Api
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
                .ConfigureLogging(logging =>
                {
                    logging.ClearProviders();
                    logging.SetMinimumLevel(LogLevel.Trace);
                });
    }
}

Now with C# 9, we can remove some of the noise and ceremony and have our code just be what the API needs to start:

using Contacts.Api;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

CreateHostBuilder(args).Build().Run();

IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
    .ConfigureLogging(logging =>
    {
        logging.ClearProviders();
        logging.SetMinimumLevel(LogLevel.Trace);
    });

This code now clearly states what the intent of the program.cs is without the extra namespace or Main method.

New Pattern Matching

While pattern matching is not new in C# 9, C# 9 did add a few more patterns.

Logical patterns:

  • and
  • or
  • not

Relations patterns:

  • <
  • >
  • <=
  • >=

These patterns help add readability to code. My favorite addition to this is the not pattern matcher. Now we can take all the instances of:

if (!person is null)

and make them more readable with:

if (person is not null)

While this one is more keystrokes, the extra couple of characters make it more readable to me than the ! operator.

Omitting the Type

The compiler is getting smarter. It’s not necessarily getting more intelligent, but getting better at understanding what you are trying to do and, again, reducing the keystrokes.

The C# 9 feature of target-typed new expressions demonstrates that the compiler is getting smarter. Now, based on the variable declaration or method signature, you can omit the type in variable declarations or usage.

Here we are declaring a variable _people of type List<Person>:

private List<Person> _people = new();

We no longer have to initialize the variable of _people with new List<Person>(). The compiler can assume that we want a new List of Person.

The same goes for methods. In the sample below, the method CalculateSalary expects a parameter of type PerformanceRating.

public Person CalculateSalary(PerformanceRating rating) 
{  
    // Omitted
}

If we wanted to initialize a new PerformanceRating object for the method without creating a variable, we can now.

var person = person.CalculateSalary(new ());

Or, we can pass in a new PerformanceRating object with one or more of its properties initialized.

var person = person.CalculateSalary(new () {Rating ="Rock Star"});

This syntax does take some getting used to. I think in the long run it leads to code that is easier to use. However, it might add more fuel to the var vs. typed variable declaration debate. :)

Wrap-up

Wow, that was a lot. C#9 added Record Types, Init Only setters, Top-Level programs, enhancements to pattern matching, and more.

I hope you take some time and play around with these new language features. Doing so will reduce your keystrokes and help your code to be readable in the long run.

Bonus: Coming Soon—C# 10

While not set in stone … As of the writing of this post, .NET 6 Preview 5 is planning on adding the following to C# 10.

  • Allow const interpolated strings.
  • Record types can seal ToString().
  • Allow both assignment and declaration in the same deconstruction.
  • Allow AsyncMethodBuilder attribute on methods.

For more, check out “What’s new in C# 10.0.”


.NET, C#
Joseph-Guadagno
About the Author

Joseph Guadagno

Joe Guadagno is a Director of Engineering at Rocket Mortgage, the US’s largest mortgage lender, based in Detroit, Michigan. He has been writing software for over 20 years, has been an active member of the .NET community, serving as a Microsoft MVP in .NET for more than 10 years. At Rocket Mortgage, he leads three software development teams building and modernizing internal services. He has spoken throughout the United States and at international events on topics ranging from Microsoft .NET, Microsoft Azure, Ionic, Bootstrap and many others (see the complete list). When not sitting at a computer, Joe loves to hang out with his family and play games, as well as checking out the latest in Home Automation. You can connect with Joe on Twitter at @jguadagno, Facebook at JosephGuadagnoNet, and on his blog at https://www.josephguadagno.net.

Related Posts

Comments

Comments are disabled in preview mode.