Taming Nullable Reference Types in C#

The term Nullable Reference Types refers to a set of features introduced in C# 8.0 that help us to write cleaner, more robust code that is resistant to unintended null dereferencing. The features that are provided make us aware of possible null reference exceptions that could occur in our applications, reducing the likelihood of encountering the dreaded NullReferenceException at runtime.

However, as wonderful as this safety net is, we may occasionally find ourselves tangled in a web of compiler warnings about potential nulls. So how can we keep our code clean and concise while still benefiting from these new features?

In this article, I will walk through some code examples and provide practical tips along the way to help remove some of the pain. Let’s dive in!

Understanding Nullable Reference Types

Nullable reference types can be used in C# version 8.0 or greater, allowing us to express when a reference type should allow null to be assigned to it. This brings reference types closer in nature to value types, in that you can now have both nullable and non-nullable reference types.

Consider the following simple example.

string? nullableString = null// Allowed.
string nonNullableString = null// Compiler Warning.
 
Console.WriteLine(nullableString);
Console.WriteLine(nonNullableString);

In the above code example, the nullableString variable is declared as ‘nullable’ by appending the ? operator after the type, before the variable name. The nonNullableString variable is considered to be ‘non-nullable’ in this example when nullable reference types are enabled.

Note that the Console.WriteLine methods calls have been added to prevent compiler warnings about unused variables.

With nullable reference types enabled, the compiler assumes that reference types are non-nullable by default and will issue warnings when it detects possible null dereferences or possible null assignments to non-nullable variables.

Enabling Nullable Reference Types

While the features provided by nullable reference types are optional, they are enabled by default when you create new projects that are using a new enough C# version. In practice, this means projects that are targeting .NET 6 or greater.

If you are working with an existing project that doesn’t have nullable reference types enabled already, the first step towards using nullable reference types effectively is to enable them for your project. Assuming that you have already upgraded your project to a new enough C# version (usually this happens as a result of upgrading your project to a newer .NET version) you can enable nullable reference types by adding the following to your project (.csproj) file.

<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>

Note that you should already have an existing PropertyGroup element within your project file, so I recommend that you add the Nullable element to it instead of creating a new group of properties.

There are other possible values for the Nullable element such as warnings and annotations. You can find additional information regarding these on the Microsoft Docs. However, the enable value is what you will usually want to go with.

Alternatively, it is possible to enable nullable reference types at a more granular level by adding #nullable enable to the top of your .cs file, as shown below.

#nullable enable
 
string? nullableString = null;

Console
.WriteLine(nullableString);

Note that you can also intersperse #nullable enable and #nullable disable throughout a .cs file to enable or disable nullable reference types in specific regions of your code.

Wrangling Compiler Warnings

Now that you have nullable reference types enabled, what happens if you’re faced with a multitude of compiler warnings? What approaches can be taken to rid ourselves of the null reference warnings?

Consider the following code example.

var user = new User { Name = "Jonathan" };
string userName = GetUserName(user);
 
string GetUserName(User user)
{
    return user.Name;
}
 
public class User
{
    public string Name { getset; }
}

For the above example, the following compiler warning will be raised regarding the Name property of the User class.

Non-nullable property ‘Name’ must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

If we are treating compiler warnings as errors (as we should) this leaves us with a few options for resolving the issue which are covered in the following subsections.

Nullable property

The first thing we could consider doing is to implement what the compiler suggests.

We could declare the Name property as nullable, as follows.

public class User
{
    public string? Name { getset; }
}

However, the problem with this approach is that we are losing the benefits of having reference types that should not be null. Although in some cases we’ll need to make our reference types nullable, ideally, we should prefer them to be non-nullable where possible.

The other problem with this approach is that the compiler warning has now shifted to the line of code within the GetUserName method, where the compiler warns of a possible null reference return (‘Name’ may be null here).

Null-coalescing check

To fix the possible null reference return warning, we could update the GetUserName method as follows.

string GetUserName(User user)
{
    return user.Name ?? "";
}

In this updated code, we are using the null-coalescing operator (??) which will check if the Name property is null and will fall back to an empty string if required. By doing this we ensure there is no possibility of a null value being returned by the method.

While this works and is a good approach for removing the compiler warning, it does mean that we would potentially need to add this sort of check to lots of places across our code base. We need to be wary of things getting messy, especially if the Name property is accessed in many places, or if lots of other nullable properties need to be added to the User class.

Null-conditional check

If our GetUserName method happened to feature a nullable User parameter (i.e. User?) we can make our code more robust by adding a null-conditional operator check, as shown below.

string GetUserName(User? user)
{
    return user?.Name ?? "";
}

By adding the null-conditional operator (?) after the user variable, before we attempt to access the Name property and complementing this with the null-coalescing operator (??) operator, we can guard against both null possibilities and return an empty string if either user or Name is null.

Null-forgiving 

Another (usually less appealing) option to work around the issue is to use the null-forgiving operator (!).

The null-forgiving operator can be used for cases where we know for sure that a value won’t be null. We can tell the compiler this by using a ! character or as it’s sometimes humorously known, the “bang” or “dammit” operator, as shown below.

string GetUserName(User user)
{
    return user.Name!;
}

In the above example, we are using the null-forgiving operator (!) to essentially tell the compiler “It’s ok, I know this will never be null”. While doing this removes the compiler warning, as you can imagine, this is a dangerous assumption to make in most cases, so the null-forgiving operator should be used sparingly to avoid runtime exceptions.

Don’t just slap a ! character into your code every time you see a nullable reference compiler warning!

Default value

The other (and the simplest) thing we could do is to resolve the warning from the original User code example is to initialise the Name property with a non-null default value.

public class User
{
    public string Name { getset; } = "";
}

In this scenario, the Name property will always start with a non-null value; therefore, there is no possibility of it being null unless it has been set to null by subsequent code.

If there is a default value that makes sense for a specific property, this is the easiest route to avoid lots of compiler warnings and removes a lot of the burden from the code that needs to work with a User object.

Null-State Attributes

Another powerful and lesser-known tool that you can utilise in relation to nullable reference types is the null-state attributes which are documented below.

Attribute Category Meaning
AllowNull Precondition A non-nullable parameter, field, or property may be null.
DisallowNull Precondition A nullable parameter, field, or property should never be null.
MaybeNull Postcondition A non-nullable parameter, field, property, or return value may be null.
NotNull Postcondition A nullable parameter, field, property, or return value will never be null.
MaybeNullWhen Conditional postcondition A non-nullable argument may be null when the method returns the specified bool value.
NotNullWhen Conditional postcondition A nullable argument won’t be null when the method returns the specified bool value.
NotNullIfNotNull Conditional postcondition A return value, property, or argument isn’t null if the argument for the specified parameter isn’t null.
MemberNotNull Method and property helper methods The listed member won’t be null when the method returns.
MemberNotNullWhen Method and property helper methods The listed member won’t be null when the method returns the specified bool value.
DoesNotReturn Unreachable code A method or property never returns. In other words, it always throws an exception.
DoesNotReturnIf Unreachable code This method or property never returns if the associated bool parameter has the specified value.

Null-State Attributes (Source: Microsoft Docs)

These attributes allow you to annotate your properties, methods, parameters etc. with additional metadata that helps the compiler understand when a value may or may not be null.

NotNullAttribute example

As an example, let’s say we update the GetUserName method as follows to return a nullable string.

var user = new User { Name = "Jonathan" };
string userName = GetUserName(user);
 
string? GetUserName(User? user)
{
    return user?.Name ?? "";
}

With this change in place, the compiler will now display a warning where the GetUserName method is called (Converting null literal or possible null value to non-nullable type).

While it is a contrived example, in this particular case, we are sure that our method will never return null. With the null-conditional and null-coalescing checks that are in place, even though the return type is a nullable string we are ensuring that a non-null string value is always returned.

Given that the GetUserName method cannot return null, we can decorate the method with the NotNullAttribute using the syntax shown in the example below to indicate that this applies to the value returned by the method.

using System.Diagnostics.CodeAnalysis;
 
var user = new User { Name = "Jonathan" };
string userName = GetUserName(user);
 
[return: NotNull]
string? GetUserName(User? user)
{
    return user?.Name ?? "";
}

With the above change in place, the compiler warning will be removed.

NotNullWhenAttribute example

A practical example of one of the null-state attributes being used by the framework can be seen in the following example.

public static bool IsNullOrWhiteSpace([NotNullWhen(false)] string? value)
{
    if (value == nullreturn true;
 
    for (int i = 0i < value.Length; i++)
    {
        if (!char.IsWhiteSpace(value[i])) return false;
    }
 
    return true;
}

The above example contains the implementation of the .NET string.IsNullOrWhiteSpace method.

The method accepts a nullable string parameter which is decorated with the NotNullWhen attribute. This tells the compiler that if the method returns false then the parameter value that was passed to it can be considered to be non-null.

This is what allows code like the following to compile without warnings.

string? userName = GetUserName(user);
 
if (!string.IsNullOrWhiteSpace(userName))
{
    Console.WriteLine(userName.ToString());
}

In the above example, if the string.IsNullOrWhiteSpace method did make use of the NotNullWhen attribute, the compiler would warn of a possible null dereference where userName.ToString() is called.

I encourage you to review where it might make sense to use null-state attributes to help simplify code that calls/accesses your methods and properties.

To Null or Not To Null

Nullable reference types are a powerful tool that can make our code safer. But remember, the goal here isn’t to eliminate nulls entirely. The aim of nullable reference types is to allow us to write more robust code that is aware of nulls and to enable us to make good decisions such that we can avoid null reference exceptions.

To sum it all up, I recommend that you embrace nullable reference types, listen to what the compiler warnings are telling you, and use these tools to help you express your intent more clearly.


I hope you enjoyed this post! Comments are always welcome and I respond to all questions.

If you like my content and it helped you out, please check out the button below 🙂

Comments