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.
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# | |
---|---|
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:
- Use sane default values whenever possible.
- Add
[Required]
data-annotation attribute on required fields which can't be assigned a sane default value. - 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# | |
---|---|
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:
- 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
orIncludeSpecification
- 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: