From f110bbc0ad084e57b9590343c438bdfab9d5e6f1 Mon Sep 17 00:00:00 2001 From: Alistair Evans Date: Wed, 6 May 2020 12:38:03 +0100 Subject: [PATCH 1/5] Add guidance for indicating nullability on DTOs --- docs/csharp/nullable-migration-strategies.md | 117 +++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/docs/csharp/nullable-migration-strategies.md b/docs/csharp/nullable-migration-strategies.md index 95ff49f4b73b4..7cf83f07fea76 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. + +## Updating Data Transfer Objects (DTOs) to indicate nullability + +Indicating the nullability of the properties of Data Transfer Objects (DTOs) in your code-base requires special consideration, to ensure that your class continues to correctly express the original design intent of the DTO. + +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 we enable nullable reference types, we want to indicate on our DTO which of the properties may be nullable, consistent with our 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 the DTO, 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 we throw 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 must 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 DTO 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 (e.g. 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) From 4ce6041f9a47a506311abec278b4baf3762ae257 Mon Sep 17 00:00:00 2001 From: Alistair Evans Date: Wed, 6 May 2020 16:28:06 +0100 Subject: [PATCH 2/5] Updated following review --- docs/csharp/nullable-migration-strategies.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/csharp/nullable-migration-strategies.md b/docs/csharp/nullable-migration-strategies.md index 7cf83f07fea76..433165d2b9654 100644 --- a/docs/csharp/nullable-migration-strategies.md +++ b/docs/csharp/nullable-migration-strategies.md @@ -181,7 +181,7 @@ public string FirstName In this scenario, if the ``FirstName`` property is accessed before it has been initialized, then we throw 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 must be configured to use [backing fields](/ef/core/modeling/backing-field) correctly. +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 @@ -197,7 +197,7 @@ 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 (e.g. by accessing the property before it has been properly initialized). +You will never observe an actual null value at runtime except as a result of a programming bug (i.e. by accessing the property before it has been properly initialized). ## See also From 145a8c65a077769bc94503794ca118e6d22727f0 Mon Sep 17 00:00:00 2001 From: Alistair Evans Date: Wed, 6 May 2020 16:39:41 +0100 Subject: [PATCH 3/5] Generalise topic to all late-initialized types --- docs/csharp/nullable-migration-strategies.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/csharp/nullable-migration-strategies.md b/docs/csharp/nullable-migration-strategies.md index 433165d2b9654..2d236f10d331e 100644 --- a/docs/csharp/nullable-migration-strategies.md +++ b/docs/csharp/nullable-migration-strategies.md @@ -87,11 +87,11 @@ What it does mean is that you can't use `T?` in a generic class or method declar 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. -## Updating Data Transfer Objects (DTOs) to indicate nullability +## Late-initialized properties, Data Transfer Objects and nullability -Indicating the nullability of the properties of Data Transfer Objects (DTOs) in your code-base requires special consideration, to ensure that your class continues to correctly express the original design intent of the DTO. +Indicating the nullability of properties that are late-initialized (i.e. set after construction) may require special consideration to ensure that your class continues to correctly express the original design intent. -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. +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: @@ -161,7 +161,7 @@ This approach only works if the library that you use to instantiate the class su 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 the DTO, to understand the extent to which it supports constructor binding. +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 @@ -185,7 +185,7 @@ You should consider that some libraries may have special considerations when usi ### Initialize the property to null -As a terser alternative to using a nullable backing field, or if the library that instantiates your DTO is not compatible with that approach, you can simply initialize the property to null directly, with the help of the null-forgiving operator (``!``): +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] From 2f6c729df924f2899abc505f16973060a7c02d2b Mon Sep 17 00:00:00 2001 From: Alistair Evans Date: Thu, 7 May 2020 15:43:12 +0100 Subject: [PATCH 4/5] Apply suggestions from code review (1) Co-authored-by: Bill Wagner --- docs/csharp/nullable-migration-strategies.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/csharp/nullable-migration-strategies.md b/docs/csharp/nullable-migration-strategies.md index 2d236f10d331e..84f4ac87a642c 100644 --- a/docs/csharp/nullable-migration-strategies.md +++ b/docs/csharp/nullable-migration-strategies.md @@ -89,7 +89,7 @@ You may want to restrict the types used for a generic type argument to be non-nu ## Late-initialized properties, Data Transfer Objects and nullability -Indicating the nullability of properties that are late-initialized (i.e. set after construction) may require special consideration to ensure that your class continues to correctly express the original design intent. +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. @@ -108,11 +108,11 @@ class Student } ``` -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 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. +The `VehicleRegistration` property is **not mandatory**, so may be null. -When we enable nullable reference types, we want to indicate on our DTO which of the properties may be nullable, consistent with our original intent: +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 @@ -129,9 +129,9 @@ class Student 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. +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. +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 @@ -179,13 +179,13 @@ public string FirstName } ``` -In this scenario, if the ``FirstName`` property is accessed before it has been initialized, then we throw an ``InvalidOperationException``, because the API contract has been used incorrectly. +In this scenario, if the `FirstName` property is accessed before it has been initialized, then we throw 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 (``!``): +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] From cc6ebdd618814007249f90747c89da19fe0b2bb7 Mon Sep 17 00:00:00 2001 From: Alistair Evans Date: Thu, 7 May 2020 15:46:10 +0100 Subject: [PATCH 5/5] Apply suggestions from code review (2) --- docs/csharp/nullable-migration-strategies.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/csharp/nullable-migration-strategies.md b/docs/csharp/nullable-migration-strategies.md index 84f4ac87a642c..ab6bc7f8c5317 100644 --- a/docs/csharp/nullable-migration-strategies.md +++ b/docs/csharp/nullable-migration-strategies.md @@ -179,7 +179,7 @@ public string FirstName } ``` -In this scenario, if the `FirstName` property is accessed before it has been initialized, then we throw an `InvalidOperationException`, because the API contract has been used incorrectly. +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. @@ -197,7 +197,7 @@ 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 (i.e. by accessing the property before it has been properly initialized). +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