Strategies for Implementing Nullable Reference Types in .NET

Strategies for Implementing Nullable Reference Types in .NET

Nullable reference types offer a way to explicitly declare whether a reference type can be null.

In C#, this feature is compile-time only. It is intended to help you avoid NullReferenceExceptions, but is completely ignored at runtime and does not technically guarantee that a reference is not null.

Opt-In at the Project Level

I recommend that you enable nullable reference types project-wide for all projects. The feature is just too valuable to pass up. Even if you think you’re good at spotting NullReferenceExceptions, there are several you aren’t seeing.

With that said, there are a few rough edges with the feature, so let me cover those and offer my suggestions.

The Constructor Problem

Traditionally, when instantiating an object, you would use an approach like this:

var person = new Person();
person.Name = "Steven";

You can also use an initializer.

var person = new Person
{
    Name = "Steven"
};

In each case, you instantiate the object and then set its name in the next operation. This approach momentarily puts the object in an incomplete state. If you declare that the Name property is not nullable, the compiler will warn you that you may have forgotten to set the name. Even if you don't forget, it will still be null between instantiation and initialization completion.

Non-Default Constructors

You can solve this problem using a non-default constructor that takes all required (non-nullable) values.

var person = new Person("Steven");

When you don't control instantiation

The above example shows how you can solve this problem, but you don't always handle instantiation.

Dependency Injection (IServiceProvider)

When the IServiceProvider instantiates a type, it injects all of the type’s dependencies in the constructor, and you should declare those dependencies as non-nullable.

System.Text.Json

The System.Text.Json API can deserialize JSON into a C# object graph using a non-default constructor with parameter names that match the property names. Properties should be marked nullable only if the JSON allows them to be null.

ASP.NET Core Model Binding

ASP.NET Core model binding requires a default constructor currently, so you are back to the original problem of instantiating an object and then setting its properties.

The key here is that the framework will instantiate the model and set its properties before you interact with this code. Consider the following example.

#nullable disable

public class MyForm
{
    [Required]
    public string MyProperty { get; set; }
}

What will happen here is:

  1. The model binder will instantiate MyForm
  2. The model binder will bind a value for MyProperty if it exists in the binding context (e.g., in the form, query string, etc.)
  3. The framework will run model state validation of MyForm, picking up the [Required] attribute and setting the model state accordingly.
  4. Your controller action method will check the model state and return the view/form if the state is invalid.
  5. Only if validation is successful will you attempt to access MyProperty.

Because of this, it’s important to indicate that for your code, MyProperty should not allow null, even though it can theoretically contain null in some cases.

So, what are your options?

Declare required fields as nullable

#nullable enable

public class MyForm
{
    [Required]
    public string? MyProperty { get; set; }
}

In this case, you are saying that MyProperty can be null until validation occurs, after which you will not continue processing if model state validation fails.

This approach is how I would tackle the problem in Kotlin, which enforces nullable-reference types at runtime. However, in C#, this is just added compiler checks for your code, so this is not too useful. The framework doesn’t care if MyProperty is null temporarily.

Null-forgiving operator

The null-forgiving operator has no effect at run time. It only affects the compiler's static flow analysis by changing the null state of the expression. At run time, expression x! evaluates to the result of the underlying expression x.
#nullable enable

public class MyForm
{
    [Required]
    public string MyProperty { get; set; } = null!;
}

In this case, you are saying that MyProperty cannot be null, and you are annotating the property as required, but the initial value before binding will be null. The ! suppresses the warning that you are assigning null since it doesn’t affect you in this case because you won’t access MyProperty unless model state validation passes.

This strategy is better than the previous one, but it still adds more clutter than I like.

Disable nullability for the class

#nullable disable

public class MyForm
{
    [Required]
    public string MyProperty { get; set; }
}

This strategy is my preferred approach. It allows me to gain most of the benefits of nullable reference types but without cluttering up my code.

EF Core

Similarly to model binding, I prefer to disable nullable reference types on EF models.

While EF core allows for non-default constructors, it cannot set navigation properties in constructors. Additionally, query results may not include navigation properties.

You may also have required properties that are set by EF, such as CreatedAt and CreatedBy properties, a TenantId, etc.

Suppose you decide to keep nullable reference types turned on for EF models. In that case, it is best to mark all navigation properties as nullable and use the null-forgiving operator to suppress null checks when you know that a query included a navigation property.

If you use EF migrations, beware that it will infer whether a field is required based on its nullability.

Configuration Binding

The configuration binding process requires a default constructor. There is a GitHub issue filed to support non-default constructors in the future.

I recommend disabling nullable reference types on configuration models.

Mastodon