diff --git a/docs/csharp/nullable-migration-strategies.md b/docs/csharp/nullable-migration-strategies.md index 95ff49f4b73b4..ab6bc7f8c5317 100644 --- a/docs/csharp/nullable-migration-strategies.md +++ b/docs/csharp/nullable-migration-strategies.md @@ -86,3 +86,120 @@ This doesn't mean you can't use a nullable type (either value type or reference What it does mean is that you can't use `T?` in a generic class or method declaration without constraints. For example, won't be changed to return `T?`. You can overcome this limitation by adding either the `struct` or `class` constraint. With either of those constraints, the compiler knows how to generate code for both `T` and `T?`. You may want to restrict the types used for a generic type argument to be non-nullable types. You can do that by adding the `notnull` constraint on that type argument. When that constraint is applied, the type argument must not be a nullable type. + +## Late-initialized properties, Data Transfer Objects and nullability + +Indicating the nullability of properties that are late-initialized, meaning set after construction, may require special consideration to ensure that your class continues to correctly express the original design intent. + +Types that contain late-initialized properties, such as Data Transfer Objects (DTOs), are often instantiated by an external library, like a database ORM (Object Relational Mapper), a deserializer, or some other component that automatically populates properties from another source. + +Consider the following DTO class, prior to enabling nullable reference types, that represents a student: + +```csharp +class Student +{ + [Required] + public string FirstName { get; set; } + + [Required] + public string LastName { get; set; } + + public string VehicleRegistration { get; set; } +} +``` + +The design intent (indicated in this case by the `Required` attribute) suggests that in this system, the `FirstName` and `LastName` properties are **mandatory**, and therefore not null. + +The `VehicleRegistration` property is **not mandatory**, so may be null. + +When you enable nullable reference types, you want to indicate on our DTO which of the properties may be nullable, consistent with your original intent: + +```csharp +class Student +{ + [Required] + public string FirstName { get; set; } + + [Required] + public string LastName { get; set; } + + public string? VehicleRegistration { get; set; } +} +``` + +For this DTO, the only property that may be null is ``VehicleRegistration``. + +However, the compiler raises `CS8618` warnings for both `FirstName` and `LastName`, indicating the non-nullable properties are uninitialized. + +There are three options available to you that resolve the compiler warnings in a way that maintains the original intent. Any of these options are valid; you should choose the one that best suits your coding style and design requirements. + +### Initialize in the constructor + +The ideal way to resolve the uninitialized warnings is to initialize the properties in the constructor: + +```csharp +class Student +{ + public Student(string firstName, string lastName) + { + FirstName = firstName; + LastName = lastName; + } + + [Required] + public string FirstName { get; set; } + + [Required] + public string LastName { get; set; } + + public string? VehicleRegistration { get; set; } +} +``` + +This approach only works if the library that you use to instantiate the class supports passing parameters in the constructor. + +In addition, a library may support passing *some* properties in the constructor, but not all. +For example, EF Core supports [constructor binding](/ef/core/modeling/constructors) for normal column properties, but not navigation properties. + +Check the documentation on the library that instantiates your class, to understand the extent to which it supports constructor binding. + +### Property with nullable backing field + +If constructor binding won't work for you, one way to deal with this problem is to have a non-nullable property with a nullable backing field: + +```csharp +private string? _firstName; + +[Required] +public string FirstName +{ + set => _firstName = value; + get => _firstName + ?? throw new InvalidOperationException("Uninitialized " + nameof(FirstName)) +} +``` + +In this scenario, if the `FirstName` property is accessed before it has been initialized, then the code throws an `InvalidOperationException`, because the API contract has been used incorrectly. + +You should consider that some libraries may have special considerations when using backing fields. For example, EF Core may need to be configured to use [backing fields](/ef/core/modeling/backing-field) correctly. + +### Initialize the property to null + +As a terser alternative to using a nullable backing field, or if the library that instantiates your class is not compatible with that approach, you can simply initialize the property to `null` directly, with the help of the null-forgiving operator (`!`): + +```csharp +[Required] +public string FirstName { get; set; } = null!; + +[Required] +public string LastName { get; set; } = null!; + +public string? VehicleRegistration { get; set; } +``` + +You will never observe an actual null value at runtime except as a result of a programming bug, by accessing the property before it has been properly initialized. + +## See also + +- [Migrate an existing codebase to nullable references](tutorials/upgrade-to-nullable-references.md) +- [Working with Nullable Reference Types in EF Core](/ef/core/miscellaneous/nullable-reference-types)