Non-Nullable Reference Type Properties with Constructors

Throughout this chapter, we have consistently disabled the C# nullable warning:

CS8618: Non-nullable field/property is uninitialized. Consider

declaring as nullable.

When you declare reference type (1) non-nullable fields or (2) non-nullable automatically implemented properties, it is obvious that these fields and properties need to be initialized before the containing object is fully instantiated. Not doing so would leave those fields and properties with a default null value—so they shouldn’t be declared as non-nullable.

The problem is that frequently the non-nullable fields and properties are initialized indirectly, outside the immediate scope of the constructor and therefore beyond the scope of the compiler’s code analysis, even if they are still, in fact, initialized, perhaps via a method or property that the constructor invokes.10 Following are some examples of this practice:

A simple property with validation that checks the value to be assigned to a field is not null before assigning it to the backing field that the compiler reports is uninitialized (see Listing 6.20).
The calculated Name property (such as Listing 6.22) sets other non-nullable properties or fields within the class.
The centralized initialization occurs in the manner shown in Listing 6.34 and Listing 6.35.
Public properties are initialized by external agents that trigger the instantiation and then initialize the properties.11

In most cases, the reference type non-nullable field or non-nullable automatically implemented property (referred to as a non-nullable field/property in this section—“reference type” is implied) is assigned indirectly via properties or methods that the constructor invokes. Unfortunately, the C# compiler doesn’t recognize an indirect assignment of a non-nullable field/property.

Furthermore, all non-nullable fields/properties need to ensure that they are not assigned a value of null. In the case of fields, they need to be wrapped in properties with the setter validation ensuring that a null value will not be assigned. (Remember that field validation relies on the guideline that we do not access fields outside of the property that wraps them.) The result is that non-nullable read-write fully implemented reference type properties should have validation preventing null assignment.

Non-nullable automatically implemented properties need to be limited to read-only encapsulation, with any values assigned during instantiation and validated as not null prior to assignment. Read-write non-nullable reference type automatically implemented properties should be avoided, especially with public setters, since preventing null assignment is problematic. Although the uninitialized non-null property compiler warning can be avoided by assigning the property from the constructor, this is not enough: The property is read-write, so it could be assigned null after instantiation, thereby voiding your intent for it to be non-nullable.

Read/Write Non-Nullable Reference Type Properties

Listing 6.36 demonstrates how to inform the compiler and avoid the false warning that a non-nullable field/property is uninitialized. The end goal is to allow the programmer to inform the compiler that the properties/fields are non-nullable so that the compiler can inform callers about the (non-)nullability of those properties/fields.

Listing 6.36: Providing Validation on Non-Nullable Property
1. public class Employee
2. {
3.     public Employee(string name)
4.     {
5.         Name = name;
6.     }
7.  
8.     public string Name
9.     {
10.         get => _Name!;
11.         set => _Name = value ?? throw new ArgumentNullException(
12.             nameof(value));
13.     }
14.     private string? _Name;
15.     // ...
16. }

The code snippet to handle non-nullable properties/fields that are not directly initialized by the constructor has several important qualities (listed in no particular order here):

The property setter includes a check for null that occurs before setting the value of the non-nullable field. In Listing 6.36, this is done by using the null-coalescing operator and throwing an ArgumentNullException if the new value is null.
The constructor invokes a method or property that indirectly assigns the non-nullable field but fails to recognize that the field is initialized to a value other than null.
The backing field is declared as nullable to avoid the compiler warning that the field is uninitialized.
The getter returns the field with a null-forgiveness operator—declaring that it is not null thanks to the setter validation.

For a non-nullable property, it is seemingly nonsensical to declare the backing field as nullable. This is necessary, however, since the compiler is oblivious to non-nullable field/property assignments outside from the constructor. Fortunately, this is a case where you, as the programmer, are justified in using the null-forgiveness operator when returning the field because of the not-null check in the setter that ensures the field is never null.

Read-Only Automatically Implemented Reference Type Properties

As pointed out earlier in this section, non-nullable automatically implemented reference-type properties need to be read-only to avoid invalid null assignments. However, you still need to validate any parameters that may be assigned during instantiation, as shown in Listing 6.37.

Listing 6.37: Validation of Non-Null Reference Type Automatically Implemented Properties
1. public class Employee
2. {
3.     public Employee(string name)
4.     {
5.         Name = name ?? throw new ArgumentNullException(nameof(name));
6.     }
7.  
8.     public string Name { get; }
9. }

One could debate whether a private setter should be allowed on non-nullable automatically implemented reference-type properties. While possible, the more appropriate question to ask is whether your class could mistakenly assign null to the property. If you don’t encapsulate the field with validation in a setter, can you be sure you won’t mistakenly assign a null value? While the compiler will verify your intent during instantiation, it is questionable that the developer will always remember to check for null in values coming into your class that should be non-nullable—as occurs in the constructor shown in Listing 6.37.

Guidelines
DO implement non-nullable read/write reference fully implemented properties with a nullable backing field, a null-forgiveness operator when returning the field from the getter, and non-null validation in the property setter.
DO assign non-nullable reference type properties before instantiation completes.
DO implement non-nullable reference type automatically implemented properties as read-only.
DO use a nullable check for all reference type properties and fields that are not initialized before instantiation completes.
required Modifier

In C# 11, the designers add the ability to mark either a field or a property as required. This designates them as “required” such that they must be assigned inside the object initializer during construction (see Listing 6.38).

Listing 6.38: Using the Required Modifier
1. public class Book
2. {
3.     public Book()
4.   {
5.       // Look up employee name...
6.       // ...
7.   }
8.  
9.     string? _Title;
10.     public required string Title
11.     {
12.         get
13.         {
14.             return _Title!;
15.         }
16.         set
17.         {
18.             _Title = value ?? throw new ArgumentNullException(nameof(value));
19.         }
20.     }
21.  
22.     string? _Isbn;
23.     public required string Isbn
24.     {
25.         get
26.         {
27.             return _Isbn!;
28.         }
29.         set
30.         {
31.             _Isbn = value ?? throw new ArgumentNullException(nameof(value));
32.         }
33.     }
34.  
35.     public string? Subtitle { getset; }
36.  
37.     // ...
38. }
39.  
40. public class Program
41. {
42.     public static void Main()
43.     {
44.         // ...
45.         Book book = new()
46.         {
47.             Isbn = "978-0135972267",
48.             Title = "Harold and the Purple Crayon"
49.         };
50.         // ...
51.     }
52. }

By adding the required modifier, we can no longer instantiate a book without supplying an object initializer that provides value for both the Isbn number and the Title (see Listing 6.39).

Listing 6.39: Specifying Required Members within the Object Initializer
1. // Error CS9035:
2. // Required member 'Book.Isbn' must be set in the object
3. // initializer or attribute constructor
4. Book book = new() { Title= "Essential C#" };
5.  
6. // ...

Notice that since we don’t include an explicit constructor for Book, we rely on the automatically generated default constructor to instantiate the book. This is ideal since the required members define how to construct the object in place of any constructor. Providing a constructor with parameters for the required members will result in having to specify the values both as constructor arguments and, redundantly, in the object initializer. A constructor with a Title parameter, for example, will result in an instantiation like this:

Book book = new("A People’s History of the United States")

{

   Title= "A People’s History of the United States",

  Isbn="978-0062397348"

};

To avoid this redundancy, you can decorate a constructor with the SetRequireParameters attribute, instructing the compiler to disable all the object initializer requirements when invoking the associated constructor. (Effectively, the SetRequireParameters attribute instructs the compiler that the developer will take care of setting all the required members so the compiler can ignore checking for initialization assignment. Unfortunately, however, there is little to no verification that such initialization did occur—the compiler doesn’t check. See Listing 6.40 for an example of how to use the SetsRequiredMembers attribute.

Listing 6.40: Disabling required Object Initialization
1. [SetsRequiredMembers]
2. public Book(int id)
3. {
4.     Id = id;
5.  
6.     // Look up book data
7.     // ...
8.     // ...
9. }

And, given such a constructor, we can instantiate a book without setting any required members:

Book book = new(42) {

     Subtitle = "The Comprehensive, Expert Guide " +

         "to C# for Programmers at All Levels" };

The disadvantage, of course, it this assumes that the constructor has valid values to assign the required members. There is no point in telling the compiler to ignore the required members if in fact there are no available ways to determine the valid required member values. Doing so will allow the constructor invocation to not set the required members while leaving invalid values. Furthermore, is there enough of an advantage in using a constructor to set non-required rather than just allowing them to be set via an object initializer. On the flip side, obviously don’t mark a value as required when the default value of the data type is valid.

As you would expect, you cannot have a required public type with a private setter. Rather, the setter on a required member must match the visibility of the type that contains the member. Since Book is public, its Isbn property setter must also be public.

One last important point to note. Adding a required member after your class is already published to production will cause existing code that instantiates the type to no longer compile because the added required members will need to be specified in the object initializer. This makes the new version incompatible with existing code; therefore, you should avoid it.

Guidelines
DO NOT use constructor parameters to initialize required properties; instead, rely on object initializer–specified values.
DO NOT use the SetRequiredParameters attribute unless all required parameters are assigned valid values during construction.
CONSIDER having a default constructor only on types with required parameters, relying on the object initializer to set both required and non-required members.
AVOID adding required members to released types to avoid breaking the compile on existing code.
AVOID required members where the default value of the type is valid.

________________________________________

10. Or potentially via an external agent like reflection; see Chapter 18.
11. Examples include the TestContext property in MSTest or objects initialized through dependency injection.
{{ snackbarMessage }}
;