You’re concerned that while your objects aren't intended to have null
members, those members will inevitably be null
during the construction of your object graph.
Ultimately, this is a really common problem. It affects, yes, deserialization, but also the creation of objects during e.g., mapping or data binding of e.g. data transfer objects or view models. Often, these members are to be null for a very brief period between constructing an object and setting its properties. Other times, they might sit in limbo during a longer period as your code e.g. fully populates a dependency data set, as required here with your interconnected object graph.
Fortunately, Microsoft has addressed this exact scenario, offering us two different approaches.
Option #1: Null-Forgiving Operator
The first approach, as @andrew-hanlon notes in his answer, is to use the null-forgiving operator. What may not be immediately obvious, however, is that you can use this directly on your non-nullable members, thus entirely eliminating your intermediary classes (e.g., PersonForSerialize
in your example). In fact, depending on your exact business requirements, you might be able to reduce your Person
class down to something as simple as:
class Person
{
internal Person() {}
public string Name { get; internal set; } = null!;
public IReadOnlyList<Person> Friends { get; internal set; } = null!;
}
Option #2: Nullable Attributes
Update: As of .NET 5.0.4 (SDK 5.0.201), which shipped on March 9th, 2021, the below approach will now yield a CS8616
warning. Given this, you are better off using the null-forgiving operator outlined above.
The second approach gives you the same exact results, but does so by providing hints to Roslyn's static flow analysis via nullable attributes. These require more annotations than the null-forgiving operator, but are also more explicit about what's going on. In fact, I actually prefer this approach just because it's more obvious and intuitive to developers otherwise unaccustomed to the syntax.
class Person
{
internal Person() {}
[NotNull, DisallowNull]
public string? Name { get; internal set; };
[NotNull, DisallowNull]
public IReadOnlyList<Person>? Friends { get; internal set; };
}
In this case, you're explicitly acknowledging that the members can be null
by adding the nullability indicator (?
) to the return types (e.g., IReadOnlyList<Person>?
). But you're then using the nullable attributes to tell consumers that even though the members are marked as nullable:
[NotNull]
: A nullable return value will never be null.
[DisallowNull]
: An input argument should never be null.
Analysis
Regardless of which approach you use, the end results are the same. Without the null-forgiving operator on a non-nullable property, you would have received the following warning on your members:
CS8618
: Non-nullable property 'Name' is uninitialized. Consider declaring the property as nullable.
Alternatively, without using the [NotNull]
attribute on a nullable property, you would have received the following warning when attempting to assign its value to a non-nullable variable:
CS8600
: Converting null literal or possible null value to non-nullable type.
Or, similarly, upon trying to call a member of the value:
CS8602
: Dereference of a possibly null reference.
Using one of these two approaches, however, you can construct the object with default (null
) values, while still giving downstream consumers confidence that the values will, in fact, not be null
—and, thus, allowing them to consume the values without necessitating guard clauses or other defensive code.
Conversely, when using either of these approaches, you will still get the following warning when attempting to assign a null
value to these members:
CS8625
: Cannot convert null literal to non-nullable reference type.
That's right: You'll even get that when assigning to the string?
property because that's what the [DisallowNull]
is instructing the compiler to do.
Conclusion
It’s up to you which of these approaches you take. As they both yield the same results, it’s a purely stylistic preference. Either way, you’re able to keep the members null
during construction, while still realizing the benefits of C#’s non-nullable types.