Skip to content

Working with Nullable Reference Types

In C#8, a feature was introduced called Nullable Reference Types (NRT). This feature allows us to treat Reference Types, meaning instances of a class, as non-nullable by default. It forces us to be explicit when we are expecting a reference type to be null.

When developing C# code, we can take advantage of NRT to make our APIs clearer on their intention on what to we expect as input/outputs, providing better and cleaner code. All projects provided by the Suite Framework embrace this feature.

NRT is enabled at the monorepo level, and is a required development feature for all teams working inside the monorepo, since it allows us to detect possible NullReferenceExceptions at compile time!

For further readings, please refer to the official docs.

Following are some guidelines we've defined when dealing with Nullable Reference Types in different contexts.

Serializable Types

Serializable types requires you to implement an specific constructor, in which you won't be able to provide initial value to your non-nullable properties. In such cases is good enough to suppress the CS8618 warning in code. This happens only if your class has non-nullable properties.

C#
        /// <summary>
        /// Initializes a new instance of the <see cref="EntityNotFoundException"/> class.
        /// </summary>
        /// <param name="info">Info, for serialization.</param>
        /// <param name="context">Context, for serialization.</param>
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
        protected EntityNotFoundException(SerializationInfo info, StreamingContext context)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
            : base(info, context)
        {
        }

IOptions Pattern

When you need to provide options having non-nullable properties, and you don't have a way to provide initial sane values, the best choice to get over the null checking warning is to use the Required data annotation in combination with the null-forgiving operator.

Bear in mind that, when using the IOptions Pattern, we can't leave the option's class without a parameterless constructor.

C#
1
2
3
4
5
public class MyModuleOptions
{
    [Required]
    public MyPropertyType SomeNotNullProperty {get; set;} = !null;
}

This way we let the options class be created, but we enforced the required field validation. Said validation will take place after all configuration providers and module's contributions got executed, but before the option's instance is resolved or injected by DI.

To summarize:

  1. Use sane default values whenever possible.
  2. Add [Required] data-annotation attribute on required fields which can't be assigned a sane default value.
  3. Use the null-forgiving operator (!) to overcome the warning on the non-null property not being initialized, only when using in combination with the [Required] attribute.

Do not use ThrowIfNull

We are working on removing ThrowIfNull, since its use is way less required when you enable NRT and are explicit on what can and cannot be null.

That being said, in many cases we don't even need to check for null. On types that are resolved through Dependency Injection, there's no need to check for nulls when doing constructor injection. The IServiceProvider will throw if it cannot instantiate your class.

The only time that you need to check for null, is when the symbol you are dealing with (method argument, field, property, etc) is nullable (meaning it has the ?) and you are about to access it. The compiler will warn you about the symbol possibly being null, and you must check for null (and possibly throw?) before accessing it to remove the compiler warning

Warning

We are gonna remove ThrowIfNull so please stop using it.

EntityFramework Entities

When using EntityFramework, entities that are mapped to the database can be a pain to work with using NRT.

Basically, we can divide it into two different kinds: primitives and relations.

Entities that only have primitive properties

If your entity is simple enough that it only has primitives properties, meaning that it does not have any HasOne or HasMany or other kind of relation, then you can use this approach.

Since NRT requires us to assign all non-nullable fields before leaving the constructor, we can use a constructor with conventionally named arguments, in order for EFCore to know which fields to "inject" into the constructor.

For example:

C#
public class CustomerWithConstructorBinding
{
    public int Id { get; set; }
    public string Name { get; set; }

    public CustomerWithConstructorBinding(string name)
    {
        Name = name;
    }
}

Notice how the name argument in the constructor CustomerWithConstructorBinding(string name) matches the Name property. If the constructor would be CustomerWithConstructorBinding(string newName), EFCore would throw when trying to hydrate our entity.

Entities with relations

Now, most of the times, our entities will have relations. In that case, we can't use the constructor argument convention, since, when building queries, we use Includes to specify which relations are loaded in memory or not, despite of them being or not required.

EFCore has some approaches for dealing with that, and we have chosen to simply use null! for initializing those relation properties. The reason why in our case is not a problem is that:

  1. Since the entity is hydrated by EFCore, if the property is non-nullable and required, it will always be present when we use an Include or IncludeSpecification
  2. If we are accessing the property, and we are not using an Include, it is considered a programming error.

Hence, we can simply assign them null! since there is no valid case when that can happen in the normal application execution. For everything else, we have tests.

Mapping classes

When mapping entities in an NRT context validation enabled, you have to express the constraint on the domain model, instead of explicitly defining the IsRequired on domain entity's properties, either by mean of fluent api or through attributes.

As can be seen here, the nullability constraint on the resulting schema defined based on the nullability state of the entity being mapped.

The rationale for doing this is that there is room for inconsistencies between IsRequired and ? (nullable notation). Model may be updated to remove the ? but EF configuration doesn't, or vice versa.

notnull generic type constraint

When defining generic classes or interfaces you can specify your intent on the generic type parameter definition, by using the notnull type constraint. Use this constraint whenever you need to specify a very general type parameter, but you do not expect it to be null.

Let's see an example:

C#
1
2
3
4
5
6
public interface IReadOnlyRepository<TId, TEntity>
    where TEntity : class
    where TId : notnull
{
    /// ...
}