使用可空引用类型处理控制器请求模型验证的正确方法是什么?

What is the correct way to handle controller request model validation with nullable reference types?

假设我有一个请求模型对象:

public class RequestModel
{
   public string? Id { get; set; }

   // other properties...
}

并且我想将该模型用于此示例控制器方法:

public ResponseModel ExampleMethod(RequestModel request)
{
   // FluentValidation validator
   _validator.ValidateAndThrow(request);

   // This method does not accept a nullable type
   _dependency.DoSomething(request.Id); // Causes "Possible null reference argument for parameter" error

   return new ResponseModel();
}

在这种情况下,将 Id 属性 标记为可为 null 是正确的(因为理论上请求不能包含它)。验证器将确保属性不为空。但是,如果我想在 DoSomething() 中使用此 属性,那么我将收到编译器警告,因为 Id 可能为空。我能找到的唯一解决方案是将外部请求对象映射到属性不可为空的某个内部版本。

然而,这将要求映射本质上执行验证(如果遇到 null,则通过在映射期间抛出某种异常),这感觉像是违反了关注点分离:

public ResponseModel ExampleMethod(RequestModel request)
{
   // FluentValidation validator
   _validator.ValidateAndThrow(request);

   // Map the request to an internal object - throw an exception if mapping fails due to null properties
   var internalModel = _mapper.Map<InternalModel>(request);

   // This method does not accept a nullable type
   _dependency.DoSomething(internalModel.Id); // No more error

   return new ResponseModel();
}

不确定我是否遗漏了什么,或者这是否是解决问题的唯一方法。我不能使 属性 不可为空,因为它需要一个默认值(例如空字符串,甚至更糟 - null!default!),这将使其无法确定 属性 是否在请求中丢失或真正作为空字符串传递。我相信 this proposal 之类的东西可能会解决问题,因为这样我就可以向编译器表明我希望这些不可为 null 的属性在初始化时(通过模型绑定)提供,而不是通过构造函数提供。我是否在这里遗漏了可空引用类型的某些方面,这会使它更容易处理?

您有一个带有可选值的模型。在 user-defined 方法中,您验证该值是否已定义。编译器无法确定此行为和警告。

为了帮助编译器,您可以像这样使用 null-forgiving 运算符:

_dependency.DoSomething(internalModel.Id!);    

与其使用 allowing null 然后手动检查,不如更好地使用 ASP 核心中的可用模型验证。在你的模型中,你应该更好地用 RequiredAttribute 标记你的 属性 并且如果你在启动代码中使用 .AddFluentValidation() 注册它,则不需要手动调用流畅的验证器。如果你的模型和验证器被正确标记,你可以在你的控制器方法中做这样的事情,你就完成了:

if(!ModelState.IsValid)
    return BadRequest(ModelState);

The only solution I can find is to map the external request object to some internal version where the properties are not nullable.

这对我来说听起来是个不错的方法。将请求模型与核心业务模型分开是很常见的。控制器操作(这似乎是)的作用主要是协调外部请求与核心业务逻辑之间的转换。

您甚至可能想让您的依赖项使用您的内部模型而不是 Id,以避免 对原始模型的痴迷 。如果你的依赖“知道”它被赋予的数字应该代表特定类型模型的 Id,它可能会更少 error-prone 使得某人不可能给它一个与它无关的数字模型类型(或直接来自他们忘记验证的输入模型的 ID)。

   _dependency.DoSomething(internalModel);

However this would require the mapping to essentially be performing validation (by throwing some kind of exception during mapping if a null is encountered) which feels like a violation of separation of concerns

输入验证是任何方法契约的隐含部分,包括 returns 值的任何方法。 int.Parse() 是否通过在返回 int 之前对错误输入抛出异常来违反关注点分离?

如果有的话,您通过使用单个模型 class 来表示两个不同的概念(输入与域模型)违反了关注点分离,这可能会因不同的原因而改变。

只有一个“问题”涉及验证输入模型并将其转换为 known-valid 域模型。这意味着您可能应该将 那个 关注点分成它自己的 method/class。

public ResponseModel ExampleMethod(RequestModel request)
{
   var internalModel = _requestValidator.Validate(request);

   _dependency.DoSomething(internalModel);

   return new ResponseModel();
}

您的 _requestValidator 使用流畅的模型验证和自动映射器这一事实是这一级别的代码(例如控制器操作)不必担心的实现细节。也许有一天您会更改它以使用显式 hand-coded 映射。您希望您的单元测试独立于此 class 的逻辑来测试 validation/mapping。