Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling temporarily null reference types when deserializing objects in C# 8.0?

I am trying C# 8.0 out, and I want to enable the null reference checking for the entire project. I am hoping I can improve my code design, and without disabling the nullability context in any code scopes.

I encountered a problem when I deserialize an object graph. The objects have references with one another but, for the final user view, all references in the object graph must have a value.

In other words, during the deserialization process, references may be null, but after all objects have finished loading, a final process will link all of the objects together, thus resolving those null references.

Workarounds

I have been able to address this using a few different techniques, and they each work as expected. They also expand the code considerably, however, by introducing a lot of extra scaffolding.

Option #1: Shadow Class

For example, I tried writing a paired class for each kind of object, using these as an intermediate object during deserialization. In these paired classes, all reference are allowed to be null. After deserialization completes, I copy all fields from these classes and convert them to the real object. Of course, with this approach, I need write a lot of extra code.

Option #2: Shadow Members

Alternatively, I tried to put a nullable field and a non-nullable property. This is similar to the previous approach, but I'm using paired members instead of paired classes. Then I add an internal setter for each field. This approach has less code than the first, but it still increases my code base considerably.

Option #3: Reflection

Traditionally, without considering performance, I would have managed deserialization using reflection so that there’s almost has no extra code on a per class basis. But writing my own parsing code has some benefits—for example, I can output more useful error messages including tips on how callers can resolve issues.

But when I introduce the nullable fields, my parsing code increases considerably—and with the sole purpose of satisfying the code analysis.

Sample Code

For the sake of demonstration, I simplified the code as much as possible; my actual classes obviously do much more than this.

class Person
{
    private IReadOnlyList<Person>? friends;

    internal Person(string name)
    {
        this.Name = name;
    }

    public string Name { get; }
    public IReadOnlyList<Person> Friends => this.friends!;

    internal SetFriends(IReadOnlyList<Person> friends)
    {
        this.friends = friends;
    }
}

class PersonForSerialize
{
    public string? Name { get; set; }
    public IReadOnlyList<string> Friends { get; set; }
}

IReadOnlyList<Person> LoadPeople(string path)
{
    PersonForSerialize[] peopleTemp = LoadFromFile(path);
    Person[] people = new Person[peopleTemp.Count];
    for (int i = 0; i < peopleTemp.Count; ++i)
    {
        people[i] = new Person(peopleTemp[i].Name);
    }

    for (int i = 0; i < peopleTemp.Count; ++i)
    {
        Person[] friends = new Person[peopleTemp[i].Friends.Count];
        for (int j = 0; j < friends.Count; ++j)
        {
            string friendName = peopleTemp[i].Friends[j];
            friends[j] = FindPerson(people, friendName);
        }

        people[i].SetFriends(friends);
    }
}

Question

Is there a way to satisfy the null reference checking in C# 8.0 for properties that are only temporarily null during deserialization without introducing a lot of extra code for every class?

like image 809
watson Avatar asked Dec 03 '22 18:12

watson


1 Answers

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.

like image 101
Jeremy Caney Avatar answered Dec 28 '22 09:12

Jeremy Caney