C# Nullable Reference Types: IntelliSense Confusion

The feature and concept of Nullable reference types were introduced in C# 8.0 and it basically made all types non-nullable by default and ensured that these types could never be assigned the value null. This is one of my favorite features in C# recently, but there are scenarios where a mixed nullable environment could cause confusion.

Confusion

To enable the assignment of the value null to a type, you have to explicitly mark that type. This uses the same concept of nullable as introduced in C# 2.0, where you, for example, make an int nullable by adding a question mark after it: int?.

When we look at a regular example service-class, we can see which benefits can be had from Nullable reference types:

public class ProductService
{
    // This method accepts a non-null string for 'productId'
    // and always returns a string
    public string FormatProductId(string productId)
    {
        // ...
    }

    // This method accepts a nullable 'formattedProductId'
    // and returns a string or null
    public string? TryGetProductName(string? formattedProductId)
    {
        // ...
    }
}

This makes things all fine and clear. We know that the method FormatProductId never returns null and that it doesn't accept null in its parameter. We also know that the method TryGetProductName returns a string which could be null and that the parameter accepts a string which could be null.

This is great, this means that we don't have to perform a null-check on productId-parameter of the FormatProductId-method, right? Well, not exactly...

Confusion: Mixed nullable environments

In an environment where all your code has Nullable reference types enabled, you can trust the output of a method and the input to its parameters. In a mixed nullable environment, things are not as straight forward, especially when you look at how IntelliSense in Visual Studio signals what to expect from the code.

Scenario 1: Modern app & legacy library

Imagine that your new modern app has Nullable reference types enabled, but you're using an external library that is legacy and does not have this enabled. This external library can be your own old library or something you've included from NuGet.

The problem now becomes that the external library is signaling, for example, that it has a method that returns a string and not a string?, so you should be able to trust that it is not null, right? Unfortunately not. Even with a local non-nullable project, IntelliSense tells me that the returned string is not null, even when it is.

Value is not null

Scenario 2: Legacy app & modern library

Imagine that you have just put together a nice library that you want others to use, either in your project, organization or publicly through NuGet. One of the best parts about using Nullable reference types is that the compiler will warn you if you try to send in a null value as a parameter to a method that explicitly states that it doesn't support null.

Nice, now you can clean out all those noisy null-checks at the top of all the methods, right? Unfortunately not. Your code might be used by another assembly (or an older version of Visual Studio), which doesn't detect the non-nullability.

In a way, this means you have to reverse the way you do null-checks in your code.

public class ProductService
{
    // This method does not accept a null-value
    // and if it does, it should throw an exception
    public string FormatProductId(string productId)
    {
        if (productId == null)
            throw new ArgumentNullException(productId);
        // ...
    }

    // This method accepts null-values
    // and should adjust its logic accordingly
    public string? TryGetProductName(string? formattedProductId)
    {
        return
            formattedProductId != null
            ? GetProductName(formattedProductId)
            : null;
    }
}

Key takeaways

My own take-aways from exploring this aspect of Nullable reference types are:

  • When building a library, always check for null in incoming method-arguments, even when Nullable reference types is enabled
  • When consuming an external legacy library, don't trust the return-type to not be null (even if it says it's not)
  • In a mixed nullable environment, the feature to guard us against NullReferenceExceptions is likely to mistakenly cause some more of them
  • When this feature is fully adopted, there will be a reduction in a lot of the overhead in null-handling code

Thoughts

Hopefully, in .NET 5, this feature is enabled by default and these kinds of confusions, and described associated errors can be avoided.

One idea for an improvement to the IntelliSense-behavior around assemblies that are not known to have Nullable reference types enabled could be to show all these types as nullable. Both because it makes things super-clear, but also because of the fact that they actually are nullable.

This change would make everything in the whole .NET Core CLR light up as nullable, but as of .NET Core 3.1, it all is nullable, by definition.

Comments