.NET Tools

Use C# 9 records and init-only properties in ReSharper and Rider 2020.3

C# 9 has brought us init-only properties and records, allowing us to work with types similarly to functional languages. Let’s take a look at some new C# 9 features and how both ReSharper and Rider 2020.3 support them.

Init-only properties

The ability to initialize C# objects without having to write a bunch of boilerplate code makes your code base more readable and easier to maintain. This is great for objects full of read-write properties, such as objects in a model or data transfer objects.

But what about objects that need immutable properties? Then you’re stuck setting those properties only during object construction. That’s because in .NET there’s two phases when bringing an object to life: construction and initialization. You set read-write property defaults during object construction, or later through a setter. For read-only (immutable) properties, call the constructor and set their values. There was no way to initialize immutable properties after the constructor runs during initialization. More specifically, we couldn’t use the object initializer to set readonly properties.

Things are different in C# 9. Now, when you want to construct and initialize an object that has immutable properties you can use C# 9’s new init-only properties. Replace the set keyword with the init keyword on properties. Then there’s no need to make a read-only property that you must initialize in the constructor as well.

Why is this distinction important? It avoids the need to create multiple variations of constructors with optional parameter variations. With the addition of the ? (nullability) operator, init-only properties can be made optional without compile-time warnings. It’s easier to read and maintain.

Because init-only properties are immutable by default, they are only mutable in the constructor and initializer. A quick-fix makes it easy to convert a property from mutable to immutable, and back.

Make init-only property mutable

This quick fix works on inherited properties too, updating the parent type!

Inherited init-only properties

Note that a constructor isn’t required here. Create the record with the init-only properties, then initialize the properties when you instantiate the record. It’s short and to the point.

public record Car()
{
    public string VIN { get; init; }
    public string Make { get; init; }
    public string Model { get; init; }
    public string Year { get; init; }
}

var sportsCar = new Car
{
    VIN = "123ZZZ456QWE95132",
    Make = "Acme Cars",
    Model = "Vroom",
    Year = "2020"
};

If you try to assign an init-only property after initialization, ReSharper and Rider will let you know that’s not allowed.

init-only props are immutable

Init-only properties are excellent for making individual properties immutable, but if you want to make an entire class immutable, go for records.

C# Records

In order to discuss C# records, we should quickly review the two kinds of types in .NET: reference and value types. Value types are the primitive types, such as int, double, decimal, DateTime, and a few others. Most types are reference types.

When you work with a reference type, you’re working with an object handle (a reference) to the data and not the data itself. When you work with value types, you work with a copy of that data – with exceptions to the rule. Therefore, value types cannot be changed or passed to functions – the copy of it is.

Records are reference types. They are a way to make an entire object immutable – not just individual properties as with init-only properties. While the code might make an assignment and a variable or object appears to change, really what’s happening is that a copy of the variable is made and that copy is changed. This happens in .NET with value types and also strings.

Let’s use a Car as an example. Defining it as a record looks like the following snippet of code. Notice the record keyword instead of class, and the init keywords applied to the properties.

public record Car
{
    public Car(string vin, string make, string model, string year)
    {
        VIN = vin;
        Make = make;
        Model = model;
        Year = year;
    }

    public string VIN { get; init; }
    public string Make { get; init; }
    public string Model { get; init; }
    public string Year { get; init; }
}

Some developers feel this syntax is antiquated and cumbersome. So C# 9 allows you to use a short syntax to declare the record, then construct and initialize by position, as shown here:

var car = new Car("123ABC456DE123456", "Acme Cars", "Vroom", "2020");
public record Car(string VIN, string Make, string Model, string Year);

There’s a similar syntax when creating derived records:

public record SportsCar(string VIN, string Make, string Model, string Year, string Profile) : Car(VIN, Make, Model, Year)

However, you may want to provide initialization for your object, rather than the short syntax only. If you’re using ReSharper or Rider and want to do this, use Explicit Property Declaration context action. Whatever way you choose, we’ve got you covered!

Convert to initialization syntax

Because of the way reference types work, you can’t just copy an object’s values. What gets copied is the reference, so both variables then refer back to the original source of data. With records, you can copy values with C#’s new with expression. And not only can you copy values but it’s far easier to make a spin-off instance only changing some of the values. For example, to copy the same make and model of car to the new year, changing only the Year property looks like this:

C# with expression

Though it’s a reference type, when you make value comparisons between two of them, the comparison works like a value type. Generally, issues with mutable types show up in programs with high concurrency and shared data.

Since copying operations works the same with record types as when copying value types, it stands to reason that comparisons will too. And they do. So calls such as Equals and GetHashCode compare the values of each property. In the following sample, the Equals method returns false, and therefore "The object’s values are not equal." is what is written to the console.

var car = new Car("123ABC456DE123456", "Acme Cars", "Vroom", "2020");
var sportsCar = car with { VIN = "123ZZZ456QWE95132"};
var equal = car.Equals(sportsCar);
Console.WriteLine(equal ? "The object's values are equal." : "The object's values are not equal.");

Conclusion

ReSharper (and Rider) keep up to date with the latest C# features, such as init-only properties and records, so you can too. While we’ve covered the main points about records and init-only properties in this post check out these interesting but less known features. Download ReSharper 2020.3 EAP and give it a try!

image description

Discover more