C# 8: Nullable Reference Types

 
 
  • Gérald Barré

This post is part of the series 'C# 8'. Be sure to check out the rest of the blog posts of the series!

After using Nullable Reference Types for several weeks, reading documentation, and studying projects like CoreFX, I wanted to share what I learned. This post explains what Nullable Reference Types are and the cases I encountered while migrating a project to this new C# 8 feature.

#Why use nullable reference types?

C# 8 brings a new feature to solve the one billion-dollar mistake. The compiler will help you find and fix most of your null-related bugs before they blow up at runtime. TypeScript has had a similar feature for a long time and it prevents so many potential issues. I'm glad that C# now has the same feature. I hope you are prepared to fix tons of warnings 🙂

#How to enable Nullable Reference Types?

First, you need to use C# 8 or later. You can open the .csproj file and add <LangVersion>8.0</LangVersion>:

MyProject.csproj (csproj (MSBuild project file))
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <LangVersion>8.0</LangVersion>
  </PropertyGroup>
</Project>

Then, you can enable Nullable Reference Types at the project level or per file. To enable it at the project level you can add <Nullable>enable</Nullable> to the .csproj:

csproj (MSBuild project file)
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

To enable per file, you can use #nullable enable where you want to enable the functionality and #nullable disable where you want to disable it.

This lets you opt in or opt out of nullable reference types where needed. This is especially useful when migrating an existing codebase, as discussed later.

#Basic examples

By default, all reference types are non-nullable. To declare a type that accepts null, add ? after the type name, similar to Nullable<T> (e.g., int?, bool?).

C#
void Sample()
{
    string str1 = null; // warning CS8625: Cannot convert null literal to non-nullable reference type.
    string? str2 = null; // ok
}

// value cannot be null
void ValueCannotBeNull(string value)
{
    _ = value.Length; // ok
}

// value can be null
void ValueMayBeNull(string? value)
{
    _ = value.Length; // warning CS8602: Dereference of a possibly null reference
}

If you call ValueCannotBeNull with a nullable type (e.g. string?), you'll get a warning:

C#
ValueCannotBeNull(null); // warning CS8625: Cannot convert null literal to non-nullable reference type

string? value1 = null;
ValueCannotBeNull(value1); // warning CS8625: Cannot convert null literal to non-nullable reference type

string? value2 = "test";
ValueCannotBeNull(value2); // ok, while the type of value2 is string?, the compiler understands the value cannot be null here

string value3 = "test";
ValueCannotBeNull(value3); // ok

The compiler understands conditional constructs and can infer that a nullable value is not null in certain code paths. For example, inside an if (value != null) block, the compiler knows the value is not null:

C#
void ValueMayBeNull(string? value)
{
    _ = value.Length; // warning CS8602: Dereference of a possibly null reference
    if (value != null)
    {
        _ = value.Length; // ok
    }
}

#The case of var type

In C#, var declares a variable without an explicit type; the compiler infers it from the right-hand expression. When nullable reference types are enabled, the compiler treats var-inferred reference types as nullable. However, flow analysis still recognizes that the value is not null when assigned a non-null value.

C#
var demo = "string value";
// equivalent to
string? demo = "string value";

This design choice avoids many compilation warnings when enabling nullable reference types. Think about the following case:

C#
var demo = "string value";
_ = demo.Length; // ok because the compiler understands the value is not null here

demo = null; // Would fail if demo was string instead of string?
_ = demo.Length; // warning CS8602: Dereference of a possibly null reference

#Sometimes you know better than the compiler that something is not null

Sometimes the compiler cannot determine that a value is not null in a given context. You can use the null-forgiving operator (!) to tell the compiler to treat the value as non-nullable and suppress the warnings.

C#
static void Main()
{
    var value = GetValue(true);
    Console.WriteLine(value!.Length); // in this case we know that value is not null, so we can use "!" to instruct the compiler "value" is not null here
}

static string? GetValue(bool returnNotNullValue)
{
    return returnNotNullValue ? "" : null;
}

You can also force a null assignment where null is not normally accepted:

C#
string a = null;     // warning
string b = null!;    // ok
string c = default!; // ok

#Generic types

Generic types are special because they can represent both value types (structs) and reference types (classes). You cannot use T? to represent a nullable type without constraining T, as it would conflict with Nullable<T> for value types. To use T?, constrain the generic to either a reference type or a value type. Unconstrained generic types can be managed using attributes, as explained later.

C#
public static void ReferenceType<T>(T? value) // [Nullable(2)]T value because T is a class
    where T : class
{
}

public static void Test2<T>(T? value) // Nullable<T> because T is a struct
    where T : struct
{
}

#Preconditions attributes: AllowNull / DisallowNull

Some cases cannot be expressed with a nullable type alone. This often occurs when a type has multiple usages. For instance, a property may accept null in the setter but always return a non-null value in the getter.

C#
private string _value = "";

[AllowNull]
public string Value
{
    get => _value;
    set => _value = value ?? "";
}

void Demo()
{
    Value = null; // ok thanks to [DisallowNull]
    _ = Value.Length; // Currently there is a warning but this should be fixed in the future version of the compiler
}

The opposite is also possible: a property can disallow null in the setter but return a nullable value in the getter.

C#
private string? _value = null;

[DisallowNull]
public string? Value
{
    get => _value;
    set => _value = value ?? throw new ArgumentNullException(nameof(value));
}

void Demo()
{
    Value = null; // not ok because of [DisallowNull]
    _ = Value.Length; // not ok as Value may be null
}

Another case involves ref parameters. You may want to accept a null input but guarantee that the parameter is set to a non-null value before the method returns.

C#
public void DemoRef([AllowNull]ref string value)
{
    value = "";
}

void A()
{
    string? value = null;
    DemoRef(ref value); // ok, DemoRef allows nullable input
    _ = value.Length;   // ok, value is non-null here the parameter type is string not string?
}

This opposite is also possible:

C#
public void DemoRef([DisallowNull]ref string? value)
{
    value = null;
}

void A()
{
    string? value = null;
    DemoRef(ref value); // warning value is null but the method doesn't allow null as input
    _ = value.Length;   // warning value is null as the method may change the value of the ref parameter to null
}

#Post-condition attributes: NotNull / MaybeNull / MemberNotNull

You can use the [NotNull] and [MaybeNull] attributes to express the nullability of the return value or the nullability of parameters when the call returns. While these attributes can be used with any type, they are most useful with unconstrained generic types where T? is not available. C# 9 update: The ? operator now works on unconstrained generic types. So, you should use T? instead of the [MayBeNull] attribute.

[MemberNotNull] indicates that after the method or property returns, the listed members are guaranteed to be non-null.

C#
// The return value may be null if T is a reference type and no item matches the condition.
[return: MaybeNull]
public static T FirstOrDefault<T>(IEnumerable<T> items, Func<T, bool> match)
{
    ...
}

// value may be null on input, but is non-null when the method returns
public static void SetValue<T>([NotNull] ref T value)
{
    ...
}

// C# 9: T? is allowed on unconstrained generic type
public static T? FirstOrDefault<T>(IEnumerable<T> items, Func<T, bool> match)
{
    ...
}
C#
public class C
{
    // Not nullable as it is initialized in the constructor using the Init method.
    private string _s;

    public C() => Init();

    [MemberNotNull(nameof(_s))]
    private void Init() => _s = "Hello";
}

You can create a method to assert the value of an object is not null as explained in this comment:

C#
public static void AssertNotNull<T>([NotNull]T? obj)
    where T : class
{
    if (obj == null)
        throw new Exception("Value is null");
}

void Sample()
{
    string? test = null;
    AssertNotNull(test);
    _ = test.ToString(); // test is not null here
}

#Conditional post-conditions attributes: NotNullWhen / MayBeNullWhen / NotNullIfNotNull / MemberNotNullWhen

Some methods have output parameters whose nullability depends on their return value. For example, Version.TryParse guarantees the parsed version is not null when the method returns true. Similarly, Dictionary.TryGetValue has an output that may be null when the method returns false. Use [NotNullWhen] and [MaybeNullWhen] to express these contracts. The [NotNullIfNotNull] attribute indicates that the return value is not null when a specific parameter is not null.

C#
// result may be null if TryGetValue returns false
public bool TryGetValue(TKey key, [MaybeNullWhen(returnValue: false)]out TValue result)
{
    ...
}

// result is not null if TryParse returns true
public static bool TryParse(string? input, [NotNullWhen(returnValue: true)] out Version? result)
{
    ...
}

// The return value is not null if the value of the path parameter is not null
// note that the name of the parameter is not validated, be careful about typo
[return: NotNullIfNotNull(parameterName: "path")]
public static string? ChangeExtension(string? path, string? extension)
{
    ...
}

public sealed class C
{
    // If IsNotNull returns true, Value is not null
    [MemberNotNullWhen(returnValue: true, member: nameof(Value))]
    public bool IsNotNull => Value != null;

    public string? Value { get; set; }
}

Support for NotNullWhen and MaybeNullWhen is limited: the compiler only tracks nullability when the method is called directly in an if statement:

C#
if (Version.TryParse("", out var version))
{
    _ = version.Major; // ok, version is not null as TryParse returns true
}

var parsed = Version.TryParse("", out var version);
if (parsed)
{
    _ = version.Major; // warning, the compiler is not able to understand that the method returns true here
}

#Flow attributes: DoesNotReturn / DoesNotReturnIf

Flow attributes tell the compiler that no nullable analysis is needed after a certain point, because the code that follows is unreachable. Here is how to use these attributes and how they affect nullable analysis.

C#
// This methods never returns
[DoesNotReturn]
public static void ThrowArgumentNullException(string argumentName)
{
    throw new ArgumentNullException(argumentName);
}

// This method does not return if condition is false
public static void MyAssert([DoesNotReturnIf(false)] bool condition)
{
    if (!condition)
        throw new Exception("condition is false");
}

These attributes can be used to assert that a variable or parameter is not null.

C#
string? value = null;    // value is null
MyAssert(value != null);
_ = value.Length;        // ok, value is not null here as MyAssert condition is false
C#
void Sample(string? value)
{
    if (value == null)
        ThrowArgumentNullException(nameof(value));

    _ = value.Length; // ok, value is not null here
}

#Generic constraint notnull

The notnull constraint prevents a generic type parameter from being nullable. For example, TKey in Dictionary<TKey, TValue> cannot be null, as passing a null key throws an ArgumentNullException. The notnull constraint enforces this at compile time.

C#
public class Dictionary<TKey, TValue> : IDictionary<TKey, TValue>, IDictionary, IReadOnlyDictionary<TKey, TValue>, ISerializable, IDeserializationCallback
    where TKey : notnull // TKey cannot be null (e.g. string? is invalid)
{
}

#Handling constructors for deserialization or frameworks such as AutoMapper

If a property is declared as non-nullable, it must be initialized in the constructor. Otherwise, the compiler raises a warning that the property may be null.

C#
public class Post
{
    public string Title { get; set; } // warning CS8618 Non-nullable property 'Title' is uninitialized
    public string Uri { get; set; }   // warning CS8618 Non-nullable property 'Uri' is uninitialized
}

You can add a constructor and initialize the properties to remove the warnings:

C#
public class Post
{
    public Post(string title, string uri)
    {
        Title = title;
        Uri = uri;
    }

    public string Title { get; set; }
    public string Uri { get; set; }
}

In some cases, a class is never instantiated directly but is created via deserialization (JSON, XML, etc.) or frameworks such as AutoMapper or IOptions in ASP.NET Core. Adding a constructor is not always practical in these scenarios. Instead, you can suppress the warnings by initializing properties with a default value and the null-forgiving operator.

C#
public class Post
{
    public string Title { get; set; } = default!; // ok, as the property is initialized
    public string Uri { get; set; } = default!;
}

#TryParse/TryGetValue pattern and generics

Using TryParseXXX or TryGetXXX methods is a common pattern. As shown earlier, [NotNullWhen(true)] indicates that the out parameter is not null when the method returns true. When the method returns false, you still need to assign a default value to the out parameter, which currently triggers a compiler warning. The workaround is the null-forgiving operator. This is a known bug that will be fixed in a future version of the compiler.

C#
public static bool TryParse<T>(string? input, [MaybeNullWhen(returnValue: false)] out T result)
{
    if (input != null)
    {
        result = Activator.CreateInstance<T>();
        return true;
    }

    // The null-forgiving operator is mandatory here. This may be fixed in a future version of the compiler.
    // https://github.com/dotnet/roslyn/issues/30953
    result = default!;
    return false;
}

#Useful extensions methods

The compiler has limitations on nullability inference, especially with generics. When using LINQ, it is common to filter out null values from sequences, but the type checker cannot always recognize when nulls have been removed.

C#
List<string?> values = new List<string?> { "a", null, "b" };
list.Where(x => x is not null)
    .Select(x => x.Length); // Warning CS8618: Non-nullable value type 'string' cannot be null

You can create extension methods to handle this case:

C#
static class EnumerableExtensions
{
    public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
        where T : class
    {
        return source.Where(item => item != null)!;
    }

    public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> items)
        where T : struct
    {
        foreach (var item in items)
            if (item.HasValue)
                yield return item.GetValueOrDefault();
    }
}

Here are some examples of how to use these extension methods:

C#
List<string?> values = new List<string?> { "a", null, "b" };
list.WhereNotNull()
    .Select(x => x.Length); // ok
C#
List<int?> values = new List<int?> { 1, null, 2 };
list.WhereNotNull()
    .Select(x => x.ToString("X")); // ok as x is a int instead of int?

#Adding nullable annotations to an existing code base

The opt-out strategy is easier to manage, as it lets you quickly see how many files still need to be annotated.

  1. Change the project configuration to use C# 8 and enable the nullable analysis

  2. Add #nullable disable at the top of each .cs file. You can use the following PowerShell script to do it automatically:

    PowerShell
    Get-ChildItem -Recurse -Filter *.cs | ForEach-Object {
        "#nullable disable`n" + (Get-Content $_ -Raw) | Set-Content $_
    }
  3. For each file, remove the directive, add nullable annotations, and fix warnings. Start with the files that have the fewest dependencies. This way, new warnings will only appear in the file where you remove the directive.

You have finished when there are no more #nullable disable in your code!

#Targeting .NET Standard 2.0 and .NET Core < 3.0

Check the next post: How to use Nullable Reference Types in .NET Standard 2.0 and .NET Framework?

#Checking nullable reference types at runtime

Nullable reference types are only checked at compile time. To inspect nullability information at runtime, you can use NullabilityInfoContext from the reflection API:

C#
var nullableContext = new System.Reflection.NullabilityInfoContext();
var propertyInfo = typeof(string).GetProperty("Length");
var info = nullableContext.Create(prop);
var nonNullable = info.ReadState == NullabilityState.NotNull;

#Resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?