Best practice for using Nullable Reference Types for DTOs [closed]
Asked Answered
D

2

42

I have a DTO which is populated by reading from a DynamoDB table. Say it looks like this currently:

public class Item
{
    public string Id { get; set; } // PK so technically cannot be null
    public string Name { get; set; } // validation to prevent nulls but this doesn't stop database hacks
    public string Description { get; set; } // can be null
}

Is there any best practice developing for dealing with this? I'd rather avoid a non-parameterless constructor since that plays badly with the ORM in the Dynamo SDK (as well as others).

It seems strange to me to write public string Id { get; set; } = ""; because this will never happen since Id is a PK and can never be null. What use would "" be even if it did somehow anyway?

So any best practice on this?

  • Should I mark them all as string? to say they can be null even though some never should be.
  • Should I initialise Id and Name with "" because they should never be null and this shows the intent even though "" would never be used.
  • Some combination of above

Please note: this is about C#8 nullable reference types If you don't know what they are best not answer.

Dimpledimwit answered 20/12, 2019 at 11:37 Comment(10)
Its a little dirty, but you can just slap #pragma warning disable CS8618 at the top of the file.Bikaner
Rather than = "", you can use = null! to initialize a property that you know will never effectively be null (when the compiler has no way of knowing that). If Description can legally be null, it should be declared a string?. Alternatively, if nullability checking for the DTO is more nuisance than help, you can simply wrap the type in #nullable disable / #nullable restore to turn off NRTs for this type only.Brython
@JeroenMostert You should put that as an answer.Bendicty
@Magnus: I'm reluctant to answer any question asking for "best practices"; such things are broad and subjective. I hope the OP can use my comment to develop their own "best practice".Brython
You can write a custom setter function to reject 'null' in case of a non-nullable reference type (i.e. throw Exception), but allow setting 'null' in case of a nullable reference type. Mark nullable with '?'.Escaut
Can you give us an example (or explanation) of why would your PK is a string?Danikadanila
@IvanGarcíaTopete: While I agree that using a string for a primary key is unusual, and may even be inadvisable depending on the circumstances, the OP's choice of data type is pretty irrelevant to the question. This could just as easily apply to a required, non-nullable string property that's not the primary key, or even a string field that's part of a composite primary key, and the question would still stand.Premiere
@IvanGarcíaTopete as opposed to an auto-incrementing integer? integers are great but they do have some drawbacks. In distributed systems globally unique identifiers (GUIDs) are preferred as it is simpler to guarantee uniqueness across multiple data stores. Also integers have the problem of being guessable which can sometimes be an issue. So why don't I use a Guid type? I just prefer the lack of restriction using a string so the unique string doesn't have to be a Guid - just any unique stringDimpledimwit
Does initializing a string as null! (e.g. public string Id { get; set; } = null!) prevent that property from being subsequently assigned a null value? Doesn't seem like it, but I want to be certain.Eakins
@MassDotNet nullable reference types do not prevent references from being assigned null. They are still reference types that can have a null value. All it does is add compiler warnings to let you know that you are potentially using a null value where a non-null value is expected. But it should be noted that deserialization code may take the type into account and reject a null value where as it would not if nullable reference types are turned off.Jordanna
P
31

As an option, you can use the default literal in combination with the null forgiving operator

public class Item
{
    public string Id { get; set; } = default!;
    public string Name { get; set; } = default!;
    public string Description { get; set; } = default!;
}

Since your DTO is populated from DynamoDB, you can use MaybeNull/NotNull postcondition attributes to control the nullability

  • MaybeNull A non-nullable return value may be null.
  • NotNull A nullable return value will never be null.

But these attributes only affect nullable analysis for the callers of members that are annotated with them. Typically, you apply these attributes to method returns, properties and indexers getters.

So, you can consider all of your properties non-nullable ones and decorate them with MaybeNull attribute, indicating them return possible null value

public class Item
{
    public string Id { get; set; } = "";
    [MaybeNull] public string Name { get; set; } = default!;
    [MaybeNull] public string Description { get; set; } = default!;
}

The following example shows the usage of updated Item class. As you can see, second line doesn't show warning, but third does

var item = new Item();
string id = item.Id;
string name = item.Name; //warning CS8600: Converting null literal or possible null value to non-nullable type.

Or you can make all properties a nullable ones and use NoNull to indicate the return value can't be null (Id for example)

public class Item
{
    [NotNull] public string? Id { get; set; }
    public string? Name { get; set; }
    public string? Description { get; set; }
}

The warning will be the same with previous example.

There is also AllowNull/DisallowNull precondition attributes for input parameters, properties and indexers setters, working on the similar way.

  • AllowNull A non-nullable input argument may be null.
  • DisallowNull A nullable input argument should never be null.

I don't think that it will help you, since your class is populated from database, but you can use them to control the nullability of properties setters, like this for the first option

[MaybeNull, AllowNull] public string Description { get; set; }

And for second one

[NotNull, DisallowNull] public string? Id { get; set; }

Some helpful details and examples of post/preconditions can be found in this devblog article

Priesthood answered 13/1, 2020 at 19:1 Comment(3)
Sorry, this is the first time I've heard of the null forgiving operator. What does setting default! do in your first code example?Eakins
@MassDotNet null-forgiving operator reference. Here it allows to avoid a compiler warning when the property is initialized with default valuePriesthood
Some learnings from EF core may help: a) If we really know a property is never null, we want to the compiler to see it as not nullable (e.g. string, no attributes) and use null-forgiving operator to "make it work". b) string / string? affects how the database scheme is generated and read - e.g. trying to read NULL from DB into a string property will throw. - s.a. learn.microsoft.com/en-us/ef/core/miscellaneous/…Alpenhorn
P
13

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, the approach of setting the value to default with the null-forgiving operator (i.e., default!), as discussed at the top of @Pavel-Anikhouski’s answer, is now your best bet.


The textbook answer in this scenario is to use a string? for your Id property, but also decorate it with the [NotNull] attribute:

public class Item
{
  [NotNull] public string? Id { get; set; }
  public string Name { get; set; }
  public string? Description { get; set; }
}

Reference: According to the documentation, the [NotNull] attribute "specifies that an output is not null even if the corresponding type allows it."

So, what exactly is going on here?

  1. First, the string? return type prevents the compiler from warning you that the property is uninitialized during construction and will thus default to null.
  2. Then, the [NotNull] attribute prevents a warning when assigning the property to a non-nullable variable or attempting to dereference it since you are informing the compiler's static flow analysis that, in practice, this property will never be null.

Warning: As with all cases involving C#'s nullability context, there's nothing technically stopping you from still returning a null value here and thus, potentially, introducing some downstream exceptions; i.e., there's no out-of-the-box runtime validation. All C# ever provides is a compiler warning. When you introduce [NotNull] you are effectively overriding that warning by giving it a hint about your business logic. As such, when you annotate a property with [NotNull], you are taking responsibility for your commitment that "this will never happen since Id is a PK and can never be null."

In order to help you maintain that commitment, you may additionally wish to annotate the property with the [DisallowNull] attribute:

public class Item
{
  [NotNull, DisallowNull] public string? Id { get; set; }
  public string Name { get; set; }
  public string? Description { get; set; }
}

Reference: According to the documentation, the [DisallowNull] attribute "specifies that null is disallowed as an input even if the corresponding type allows it."

This may not be relevant in your case since the values are being assigned via the database, but the [DisallowNull] attribute will give you a warning if you ever attempt to assign a null(able) value to Id, even though the return type otherwise allows for it to be null. In that regard, Id would act exactly like a string as far as C#'s static flow analysis is concerned, while also allowing the value to remain uninitialized between the construction of the object and the population of the property.

Note: As others have mentioned, you can also achieve a virtually identical outcome by assigning the Id a default value of either default! or null!. This is, admittedly, somewhat of a stylistic preference. I prefer to use the nullability annotations as they're more explicit and provide granular control, whereas it's easy to abuse the ! as a way of shutting up the compiler. Explicitly initializing a property with a value also nags at me if I know I'm never going to use that value—even if it is the default.

Premiere answered 13/1, 2020 at 23:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.