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:
- The model binder will instantiate
MyForm
- The model binder will bind a value for
MyProperty
if it exists in the binding context (e.g., in the form, query string, etc.) - The framework will run model state validation of
MyForm
, picking up the[Required]
attribute and setting the model state accordingly. - Your controller action method will check the model state and return the view/form if the state is invalid.
- 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, expressionx!
evaluates to the result of the underlying expressionx
.
#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.