Best practice when using C# 8.0 nullable reference types with deserialization?

后端 未结 3 1944
长发绾君心
长发绾君心 2021-01-15 03:36

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 nullabil

相关标签:
3条回答
  • 2021-01-15 04:08

    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 entities, data transfer objects, and view models. Often, these members are to be null for a very brief period between constructing the object and setting the 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., SerializedPerson 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

    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]: A nullable 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 either of 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.

    0 讨论(0)
  • 2021-01-15 04:08

    While certainly highlighted due to non-nullable defaults in C# 8, this is really a common 'circular dependency' problem that has always existed with interdependent construction.

    As you found, one standard solution is to use a restricted setter. With C# 8, you may want to use a 'null-forgiving' null! during deserialization of the object graph - allowing you to differentiate an unconstructed set from a valid empty set, and minimize allocation.

    Example:

    class Person
    {
        internal Person(string name, IReadOnlyList<Person> friends)
        {
            Name = name; Friends = friends
        }
    
        public string Name { get; }
        public IReadOnlyList<Person> Friends {get; internal set;}
    }
    
    class SerializedPerson { ... }
    
    IEnumerable<Person> LoadPeople(string path)
    {
        var serializedPeople = LoadFromFile(path);
    
        // Note the use of null!
        var people = serializedPeople.Select(p => new Person(p.Name, null!));
    
        foreach(var person in people)
        {
            person.Friends = GetFriends(person, people, serializedPeople);
        }
    
        return people;
    }
    
    0 讨论(0)
  • 2021-01-15 04:09

    If I understand correctly, your question boils down to:

    How do I avoid compiler warnings

    CS8618: Non-nullable property 'Name' is uninitialized. Consider declaring the property as nullable.

    for simple model classes which are used for serialization?

    You can solve the problem by creating a default constructor for which you suppress the warning. Now you want to make sure that this constructor is only used by your deserialization routine (e.g. System.Text.Json or Entity Framework). To avoid unintentional use add annotation [Obsolete] with parameter error=true which would raise compiler error CS0618.

    As code:

    public class PersonForSerialize
    {
    #pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
        [Obsolete("Only intended for de-serialization.", true)]
        public PersonForSerialize()
    #pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
        {
        }
    
        // Optional constructor
        public PersonForSerialize(string name, IReadOnlyList<string> friends)
        {
            Name = name;
            Friends = friends;
        }
    
        public string Name { get; set; }
        public IReadOnlyList<string> Friends { get; set; }
    }
    

    Note1: You can let Visual Studio auto-generate the optional constructor using a quick action.

    Note2: If you really mean to use the constructor marked as obsolete, you need to remove the error=true parameter. Now you can suppress the warning when calling the parameter-less constructor via #pragma warning disable CA1806.

    0 讨论(0)
提交回复
热议问题