使用可空引用类型处理DTO的最佳实践

42

我有一个DTO,通过读取DynamoDB表来填充它。当前看起来是这样的:

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
}

有没有针对这个问题的最佳实践?我宁愿避免使用非参数化构造函数,因为它与Dynamo SDK(以及其他ORM)不兼容。
对我来说,写public string Id { get; set; } = "";似乎很奇怪,因为这永远不会发生,因为Id是主键,永远不可能为空。即使它以某种方式出现,""有什么用呢?
那么有没有任何最佳实践呢?
- 我应该将它们全部标记为string?,表示它们可以为空,即使有些永远不应该为空。 - 我应该使用""初始化IdName,因为它们永远不应该为空,并且这显示了意图,即使""永远不会被使用。 - 上述两者的某种组合
请注意:这是关于C#8的nullable reference types,如果您不知道它们是什么,则最好不要回答。

9
不必使用="",而是可以使用=null!来初始化一个属性,这样你就知道它永远不会实际上为空(当编译器无法知道时)。如果Description可能为null,那么应该声明为string?。或者,如果DTO的空值检查更令人困扰而不是有帮助的话,你可以简单地将该类型用#nullable disable / #nullable restore包装起来,以仅关闭此类型的NRTs。 - Jeroen Mostert
1
@JeroenMostert 你应该把那个作为答案。 - Magnus
4
@Magnus:我不太愿意回答任何要求“最佳实践”的问题;这类问题通常比较宽泛而且很主观。希望提问者可以利用我的评论来制定自己的“最佳实践”。 - Jeroen Mostert
1
虽然我同意使用字符串作为主键是不寻常的,而且根据情况可能不可取,但原帖所选择的数据类型与问题本身并不相关。这个问题同样适用于必需的、非空的字符串属性(不是主键),或者甚至是组合主键的一部分的字符串字段,并且问题仍将存在。 - Jeremy Caney
2
@MassDotNet 可空引用类型并不能防止将“null”分配给引用。它们仍然是可包含Null值的引用类型。它唯一做的是添加编译器警告,让您了解在期望非空值的位置使用空值的潜在风险。但需要注意的是,反序列化代码可能会考虑该类型并拒绝一个空值,而如果关闭可空引用类型则不会这样。 - juharr
显示剩余5条评论
2个回答

30
作为一种选择,您可以使用default文本与null forgiving operator结合使用。
public class Item
{
    public string Id { get; set; } = default!;
    public string Name { get; set; } = default!;
    public string Description { get; set; } = default!;
}

由于您的DTO是从DynamoDB填充的,因此您可以使用MaybeNull/NotNull后置条件属性来控制可空性。
  • MaybeNull非空返回值可能为空。
  • NotNull可空返回值永远不会为空。
但是这些属性仅影响带有注释成员的调用方的可空性分析。通常,您将这些属性应用于方法返回、属性和索引器getter。
因此,您可以将所有属性视为非空属性,并使用MaybeNull属性进行修饰,指示它们返回可能的null值。
public class Item
{
    public string Id { get; set; } = "";
    [MaybeNull] public string Name { get; set; } = default!;
    [MaybeNull] public string Description { get; set; } = default!;
}


以下示例显示了更新的Item类的用法。正如您所看到的,第二行不显示警告,但第三行会显示。
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.

或者您可以将所有属性都设置为可空属性,并使用NoNull来指示返回值不能为null(例如Id

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

警告与前面的示例相同。
还有AllowNull/DisallowNull预置条件属性用于输入参数、属性和索引器设置器,工作方式类似。
  • AllowNull非空输入参数可能为空。
  • DisallowNull可为空的输入参数不应为空。
我认为这对你没有帮助,因为你的类是从数据库中填充的,但你可以使用它们来控制属性设置器的可空性,例如第一选项。
[MaybeNull, AllowNull] public string Description { get; set; }

And for second one 而对于第二个
[NotNull, DisallowNull] public string? Id { get; set; }

在这篇devblog article中,可以找到一些有用的细节和关于前置条件和后置条件的示例。


抱歉,这是我第一次听说null forgiving运算符。在你的第一个代码示例中,设置default!会做什么? - Mass Dot Net
@MassDotNet null-forgiving operator 参考。在这里,它允许在属性使用默认值初始化时避免编译器警告。 - Pavel Anikhouski
1
一些来自 EF Core 的经验可以帮助我们:a)如果我们确实知道某个属性永远不会为空,我们希望编译器将其视为非空(例如字符串,没有属性),并使用 null-forgiving 运算符来“使其工作”。b)string / string?会影响数据库方案的生成和读取 - 例如,尝试从数据库中读取 NULL 到一个字符串属性将会抛出异常。- 参见 https://learn.microsoft.com/en-us/ef/core/miscellaneous/nullable-reference-types - Christoph Lütjen

13
更新:截至2021年3月9日发布的.NET 5.0.4(SDK 5.0.201),下面的方法现在将产生一个CS8616警告。鉴于此,使用空值判断运算符(即default!)将值设置为默认值,如@Pavel-Anikhouski's answer中讨论的那样,现在是最好的选择。
在这种情况下,标准答案是为您的Id属性使用一个string?,但同时用[NotNull]属性修饰它。
public class Item
{
  [NotNull] public string? Id { get; set; }
  public string Name { get; set; }
  public string? Description { get; set; }
}

根据文档[NotNull]属性“指定即使相应的类型允许,输出也不为null。”
那么这里到底发生了什么?
首先,string?返回类型防止编译器在构造期间警告您该属性未初始化,并因此默认为null
然后,[NotNull]属性防止将属性分配给非可为空变量或尝试对其进行解引用时出现警告,因为您正在向编译器的静态流分析提供实践中此属性永远不会为null的信息。
由于C#的可空性上下文涉及的所有情况都存在潜在的异常风险,因此请注意:从技术上讲,仍然可以返回null值,并因此可能引入一些下游异常;即,没有开箱即用的运行时验证。 C#提供的只是编译器警告。当您引入[NotNull]时,实际上是通过向编译器提供有关业务逻辑的提示来覆盖该警告。因此,当您使用[NotNull]注释属性时,您需要对“由于Id是PK且永远不会为null”这一承诺负责。
为了帮助您保持这一承诺,您可能还希望使用[DisallowNull]属性对该属性进行注释:
public class Item
{
  [NotNull, DisallowNull] public string? Id { get; set; }
  public string Name { get; set; }
  public string? Description { get; set; }
}

参考资料:根据文档[DisallowNull]属性“指定即使相应的类型允许,也禁止将null作为输入”。
在您的情况下可能不相关,因为值是通过数据库赋值的,但是[DisallowNull]属性会在您尝试将可空的null值分配给Id时发出警告,即使返回类型允许它为空。在这方面,就C#的静态流分析而言,Id的行为与string完全相同,同时在对象构造和属性填充之间允许该值保持未初始化状态。
注意:正如其他人提到的那样,您还可以通过将Id分配默认值default!null!来实现几乎相同的结果。 这,无可否认,有点风格偏好。 我更喜欢使用可空性注释,因为它们更明确并提供细粒度控制,而滥用!作为关闭编译器的方法很容易。即使它是默认值,显式地初始化属性的值也会让我感到不安,因为我知道我永远不会使用那个值。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接