How-To's

Nullable Reference Types: Migrating a Codebase – A Look at New Language Features in C# 8

ReSharper and Rider support for C# 8 It’s been a while since our last post in this C# 8 language features series. Before we jump into nullable reference types, here is a quick (updated) recap of our roadmap:

ReSharper and Rider users have been enjoying static analysis for nullable types for over 10 years already. Meanwhile, this feature is now also supported by the compiler itself, and is commonly known as nullable reference types (NRT).

It turned out that adding support for C#’s NRT inside ReSharper and Rider was quite challenging, especially since the innards are still a moving target, and our developers were able to find lots of edge cases. All those years we’ve worked with nullability analysis have finally paid off!

In this post, we won’t be talking about the boring things, like that we’ve re-implemented related compiler warnings with our own fast code inspections that show up at the moment you’ve finished typing. Rather, we will focus on the new convenient quick-fixes that ReSharper and Rider are offering, and how everything works together to help migrate a codebase to use nullable reference types.

For users of JetBrains Annotations for null-checking, we will also briefly discuss the differences with Roslyn’s own attributes in a follow-up post. Let the journey begin!

How to deal with cascading warnings

As mentioned in other articles, migrating to nullable reference types often causes a lot of propagating warnings. This is a good opportunity to note that ReSharper and Rider come with actions to jump to the next issue in either the current file or the complete solution. For the latter to work, we need to include warnings in solution-wide analysis. Both the fast code analysis and these shortcuts enable us to efficiently introduce NRT to our codebase.

Jump to Next Error

Tip: If there are too many warnings, we can temporarily raise the severity for NRT warnings to the error level.

Codebase meets nullable reference types

Since its early public discussion, the Person class has always served as a good example to illustrate the power of nullable reference types. Let’s not make an exception here. As a first step, we’ve already declared the middleName parameter as being nullable:

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

    public Person(string firstName, string? middleName, string lastName)
    {
        FirstName = firstName;
        // CS8601: Possible null reference assignment.
        MiddleName = middleName;
        LastName = lastName;
    }
}

The property initialization of MiddleName now shows a possible null reference assignment (CS8601) warning. A common task during our migration is to change the type of the property to its nullable form to bring everything in line:
Change property type to nullable reference type

Let’s consider another example of an IPersonRepository interface that provides an API to search for a Person instance by name. In a simpler form, its definition and implementation could look something like this:

public interface IPersonRepository
{
    Person GetPerson(string lastName);
}

public class PersonRepository : IPersonRepository
{
    public Person GetPerson(string lastName)
    {
        // CS8603: Possible null reference return.
        return _people.SingleOrDefault(x => x.LastName == lastName);
    }
}

So our API could return null, causing the compiler to issue a possible null reference return (CS8603) warning. In order to fix this, we need to change the method signature to use Person? as return type. Easy enough. And while we’re at it, let’s also update the derived interface:
Change signature to nullable reference type from usage

Depending on our preferences, we could have also changed the signature directly. Here we can choose to either use the return type from the base type, or to adapt the base declaration:
Change signature to nullable reference type from mismatch

Note that overriding and implementing members always allows to strengthen the contract for output values and weaken it for input values (similar to covariance and contravariance).

Moving forward, we might use our IPersonRepository interface in a situation like the following, where we first make sure that our retrieved Person object is valid (not null) and then continue working with it:

public void M()
{
    var p = _repository.GetPerson("Mads");
    if (IsValid(p))
    {
        // CS8602: Dereference of a possibly null reference.
        Console.WriteLine($"Great job {p.FirstName}!");
    }
}

private bool IsValid(Person? person)
{
    return !string.IsNullOrEmpty(person?.FirstName) &&
           !string.IsNullOrEmpty(person?.LastName);
}

Again we’re facing a propagating warning – dereference of a possibly null reference (CS8602). Generally, we could silence the compiler by using the null-conditional operator (?.). However, since we know that our object won’t be null, we can be more explicit and make use of the null-forgiving operator (!), which suppresses the error and essentially tells the compiler that we know better:
Use null-forgiving operator to suppress nullable reference type warning

Another way of fixing this is to add a [NotNullWhen(true)] attribute to the person parameter. This way, the compiler knows that after IsValid has returned true, the object can be dereferenced. ReSharper has been nicely covering such situations way before NRT. We can just add a ContractAnnotation attribute to our IsValid method, and define the contract => true, person: notnull; => false, which would mean that if the method returns true, we can also be sure that our object won’t be null.

Let’s look at another NRT situation in our codebase. We have a WorkItem that must be assigned to a person that we’ve just received from our IPersonRepository:

public void N()
{
    var person = _repository.GetPerson("Doe");
    var task = new WorkItem("Fix all the bugs!");
    // CS8604: Possible null reference argument.
    task.AssignTo(person);
}

internal class WorkItem
{
    public WorkItem(string description) => Description = description;
    public string Description { get; }
    public void AssignTo(Person person) { }
}

The AssignTo method can’t deal with null values, so the compiler will issue a warning of a possible null reference argument (CS8604). With another quick-fix, we can just use the null-coalescing operator (??) to provide a fallback value. This also works nicely with ReSharper’s and Rider’s smart completion:
Use null-coalescing operator with fallback value

No manager at hand? No worries! We can also just use a throw expression to throw an exception. If the argument is a parameter of the enclosing function, a default of ArgumentNullException will be used. Otherwise, we can define the exception type ourselves:
Use null-coalescing operator with throw expression

As we can see, the new nullable reference types also work very nicely with some of the previously introduced language features. And with ReSharper and Rider, the transitions to better code are just an Alt+Enter keystroke away.

We hope these new quick-fixes prove helpful in easily migrating your codebase to use nullable reference types and take advantage of the new language feature. Tell us how it goes, what you think, or if you’re missing anything. Download ReSharper 2020.1 or check out Rider 2020.1.

image description