Chapter13:VALIDATION

13 VALIDATION

While writing API actions, we have a set of rules that we need to check. If we take a look at the Company class, we can see different data annotation attributes above our properties:
在编写 API 操作时,我们需要检查一组规则。如果我们看一下 Company 类,我们可以在属性上方看到不同的数据注释属性:

Alt text

Those attributes serve the purpose to validate our model object while creating or updating resources in the database. But we are not making use of them yet.
这些属性用于在数据库中创建或更新资源时验证模型对象。但我们还没有利用它们。

In this chapter, we are going to show you how to validate our model objects and how to return an appropriate response to the client if the model is not valid. So, we need to validate the input and not the output of our controller actions. This means that we are going to apply this validation to the POST, PUT, and PATCH requests, but not for the GET request.
在本章中,我们将向您展示如何验证我们的模型对象,以及如何在模型无效时向客户端返回适当的响应。因此,我们需要验证控制器操作的输入,而不是输出。这意味着我们将把此验证应用于 POST、PUT 和 PATCH 请求,但不应用于 GET 请求。

To validate against validation rules applied by Data Annotation attributes, we are going to use the concept of ModelState. It is a dictionary containing the state of the model and model binding validation.
为了根据数据注释属性应用的验证规则进行验证,我们将使用 ModelState 的概念。它是一个字典,包含模型的状态和模型绑定验证。

Once we send our request, the rules defined by Data Annotation attributes are checked. If one of the rules doesn’t check out, the appropriate error message will be returned. We are going to use the ModelState.IsValid expression to check for those validation rules.
发送请求后,将检查数据注释属性定义的规则。如果其中一个规则未签出,则将返回相应的错误消息。我们将使用ModelState.Is用于检查这些验证规则的有效表达式。

Finally, the response status code, when validation fails, should be 422 Unprocessable Entity. That means that the server understood the content type of the request and the syntax of the request entity is correct, but it was unable to process validation rules applied on the entity inside the request body.
最后,验证失败时的响应状态代码应为 422 无法处理的实体。这意味着服务器理解请求的内容类型和请求实体的语法是正确的,但它无法处理应用于请求正文内实体的验证规则。

So, with all this in mind, we are ready to implement model validation in our code.
因此,考虑到所有这些,我们已准备好在代码中实现模型验证。

13.1 Validation while Creating Resource

Let’s send another request for the CreateEmployee action, but this time with the invalid request body:
让我们为 CreateEmployee 操作发送另一个请求,但这次使用无效的请求正文:

{
    "name":null,
    "age":29,
    "position":null
}

https://localhost:5001/api/companies/53a1237a-3ed3-4462-b9f0-5a7bb1056a33/employees
Alt text

And we get the 500 Internal Server Error, which is a generic message when something unhandled happens in our code. But this is not good. This means that the server made an error, which is not the case. In this case, we, as a consumer, sent a wrong model to the API — thus the error message should be different.
我们得到 500 内部服务器错误(VisualStudio提示异常,然后Postman收到500错误),这是代码中发生未处理某些事情时的通用消息。但这并不好。这意味着服务器犯了一个错误,但事实并非如此。在在这种情况下,作为消费者,我们向 API 发送了一个错误的模型——因此错误消息应该有所不同。

In order to fix this, let’s modify our EmployeeForCreationDto class because that’s what we deserialize the request body to:
为了解决这个问题,让我们修改我们的 EmployeeForCreationDto 类,因为这就是我们将请求正文反序列化为的内容:

public class EmployeeForCreationDto
{
    [Required(ErrorMessage = "Employee name is a required field.")]
    [MaxLength(30, ErrorMessage = "Maximum length for the Name is 30 characters.")]
    public string Name { get; set; }

    [Required(ErrorMessage = "Age is a required field.")]
    public int Age { get; set; }

    [Required(ErrorMessage = "Position is a required field.")]
    [MaxLength(20, ErrorMessage = "Maximum length for the Position is 20 characters.")]
    public string Position { get; set; }
}

Once we have the rules applied, we can send the same request again:
应用规则后,我们可以再次发送相同的请求:
https://localhost:5001/api/companies/53a1237a-3ed3-4462-b9f0-5a7bb1056a33/employees

Alt text

You can see that our validation rules have been applied and verified as well. ASP.NET Core validates the model object as soon as the request gets to the action.
您可以看到我们的验证规则也已应用和验证。ASP.NET Core 会在请求到达操作后立即验证模型对象。

But the status code for this response is 400 Bad Request. That is acceptable, but as we said, there is a status code that better fits this kind of situation. It is a 422 Unprocessable Entity.
但此响应的状态代码为 400 错误请求。这是可以接受的,但正如我们所说,有一个状态代码更适合这种情况。它是一个 422 无法处理的实体。

To return 422 instead of 400, the first thing we have to do is to suppress the BadRequest error when the ModelState is invalid. We are going to do that by adding this code into the Startup class in the ConfigureServices method:
要返回 422 而不是 400,我们要做的第一件事是在 ModelState 无效时抑制 BadRequest 错误。我们将通过将以下代码添加到 ConfigureServices 方法中的 Startup 类中来做到这一点:

// Startup.cs

services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressModelStateInvalidFilter = true;
});

With this, we are suppressing a default model state validation that is implemented due to the existence of the [ApiController] attribute in all API controllers. Before our request reaches the action, it is validated with the [ApiController] attribute. So this means that we can solve the same problem differently, by commenting out or removing the [ApiController] attribute only, without additional code for suppressing validation. It’s all up to you.
这样,我们将禁止显示由于所有 API 控制器中存在 [ApiController] 属性而实现的默认模型状态验证。在我们的请求到达操作之前,将使用 [ApiController] 属性对其进行验证。因此,这意味着我们可以通过仅注释掉或删除 [ApiController] 属性来以不同的方式解决相同的问题,而无需额外的代码来抑制验证。这一切都取决于你。

Then, we have to modify our action:
然后,我们必须修改我们的控制器:


[HttpPost]
public IActionResult CreateEmployeeForCompany(Guid companyId, [FromBody] EmployeeForCreationDto employee)
{
    if (employee == null)
    {
        _logger.LogError("EmployeeForCreationDto object sent from client is null.");
        return BadRequest("EmployeeForCreationDto object is null");
    }
    if (!ModelState.IsValid)
    {
        _logger.LogError("Invalid model state for the EmployeeForCreationDto object");
        return UnprocessableEntity(ModelState);
    }

    var company = _repository.Company.GetCompany(companyId, trackChanges: false);
    if (company == null)
    {
        _logger.LogInfo($"Company with id: {companyId} doesn't exist in the database.");
        return NotFound();
    }
    var employeeEntity = _mapper.Map<Employee>(employee);

    _repository.Employee.CreateEmployeeForCompany(companyId, employeeEntity);
    _repository.Save();
    var employeeToReturn = _mapper.Map<EmployeeDto>(employeeEntity);

    return CreatedAtRoute("GetEmployeeForCompany", new { companyId, id = employeeToReturn.Id }, employeeToReturn);
}
``

And that is all.
仅此而已。

Let’s send our request one more time:
让我们再发送一次请求:

https://localhost:5001/api/companies/53a1237a-3ed3-4462-b9f0-5a7bb1056a33/employees

Alt text

Let’s send an additional request to test the max length rule:
让我们发送一个额外的请求来测试最大长度规则:

{
    "name":"Michael Patel",
    "age":29,
    "position":"Some position with invalid length"
}

https://localhost:5001/api/companies/53a1237a-3ed3-4462-b9f0-5a7bb1056a33/employees

Alt text

Excellent. It is working as expected.
非常好。它正在按预期工作。

The same actions can be applied for the CreateCompany action and CompanyForCreationDto class — and if you check the source code for this chapter, you will find it implemented.
相同的操作可以应用于 CreateCompany 操作和 CompanyForCreationDto 类 — 如果您查看本章的源代码,您会发现它已实现。

13.1.1 Validating Int Type

Let’s create one more request with the request body without the age property:
让我们再创建一个请求,请求正文没有年龄属性:

{
    "name":null,
    "position":"Some position with invalid length"
}

https://localhost:5001/api/companies/53a1237a-3ed3-4462-b9f0-5a7bb1056a33/employees

Alt text

We can clearly see that the age property hasn’t been sent, but in the response body, we don’t see the error message for the age property next to other error messages. That is because the age is of type int and if we don’t send that property, it would be set to a default value, which is 0.
我们可以清楚地看到 age 属性尚未发送,但在响应正文中,我们在其他错误消息旁边看不到 age 属性的错误消息。这是因为年龄是 int 类型,如果我们不发送该属性,它将被设置为默认值,即 0。

So, on the server-side, validation for the Age property will pass, because it is not null.
因此,在服务器端,Age 属性的验证将通过,因为它不为 null。

To prevent this type of behavior, we have to modify the data annotation attribute on top of the Age property in the EmployeeForCreationDto class:
为了防止这种类型的行为,我们必须修改 EmployeeForCreationDto 类中 Age 属性之上的数据注释属性:

[Range(18, int.MaxValue, ErrorMessage = "Age is required and it can't be lower than 18")] 
public int Age { get; set; }

Now, let’s try to send the same request one more time:
现在,让我们尝试再次发送相同的请求:
https://localhost:5001/api/companies/53a1237a-3ed3-4462-b9f0-5a7bb1056a33/employees

Alt text

Now, we have the Age error message in our response.
现在,我们的响应中有 Age 错误消息。

If we want, we can add the custom error messages in our action:
如果需要,我们可以在操作中添加自定义错误消息:

ModelState.AddModelError(string key, string errorMessage)

With this expression, the additional error message will be included with all the other messages.
使用此表达式,其他错误消息将包含在所有其他消息中。

13.2 Validation for PUT Requests

The validation for PUT requests shouldn’t be different from POST requests (except in some cases), but there are still things we have to do to at least optimize our code.
PUT请求的验证不应该与POST请求不同(在某些情况下除外),但我们仍然需要做一些事情来至少优化我们的代码。

But let’s go step by step.
但是,让我们一步一步来

First, let’s add Data Annotation Attributes to the EmployeeForUpdateDto class:
首先,让我们将数据注释属性添加到 EmployeeForUpdateDto class:

//EmployeeForUpdateDto.cs

using System.ComponentModel.DataAnnotations;

namespace Entities.DataTransferObjects
{
    public class EmployeeForUpdateDto
    {
        [Required(ErrorMessage = "Employee name is a required field.")]
        [MaxLength(30, ErrorMessage = "Maximum length for the Name is 30 characters.")]
        public string Name { get; set; }

        [Range(18, int.MaxValue, ErrorMessage = "Age is required and it can't be lower than 18")]
        public int Age { get; set; }

        [Required(ErrorMessage = "Position is a required field.")]
        [MaxLength(20, ErrorMessage = "Maximum length for the Position is 20 characters.")]
        public string Position { get; set; }
    }
}

Once we have done this, we realize we have a small problem. If we compare this class with the DTO class for creation, we are going to see that they are the same. Of course, we don’t want to repeat ourselves, thus we are going to add some modifications.
一旦我们这样做了,我们就会意识到我们有一个小问题。如果我们将此类与用于创建的 DTO 类进行比较,我们将看到它们是相同的。当然,我们不想重复自己,因此我们将添加一些修改。

Let’s create a new class in the DataTransferObjects folder:
让我们在 DataTransferObjects 文件夹中创建一个新类:

using System;
using System.ComponentModel.DataAnnotations;

namespace Entities.DataTransferObjects
{
    public abstract class EmployeeForManipulationDto
    {
        [Required(ErrorMessage = "Employee name is a required field.")]
        [MaxLength(30, ErrorMessage = "Maximum length for the Name is 30 characters.")]
        public string Name { get; set; }

        [Range(18, int.MaxValue, ErrorMessage = "Age is required and it can't be lower than 18")]
        public int Age { get; set; }

        [Required(ErrorMessage = "Position is a required field.")]
        [MaxLength(20, ErrorMessage = "Maximum length for the Position is 20 characters.")]
        public string Position { get; set; }
    }
}

We create this class as an abstract class because we want our creation and update DTO classes to inherit from it:
我们将这个类创建为抽象类,因为我们希望我们的创建和更新 DTO 类继承自它:

public class EmployeeForUpdateDto : EmployeeForManipulationDto { } 

public class EmployeeForCreationDto : EmployeeForManipulationDto { }

Now, we can modify the UpdateEmployeeForCompany action by adding the model validation right after the null check:
现在,我们可以通过在空检查后立即添加模型验证来修改 UpdateEmployeeForCompany 控制器:

if(employee == null)
{ _logger.LogError("EmployeeForUpdateDto object sent from client is null."); 
return BadRequest("EmployeeForUpdateDto object is null"); 
} 
if (!ModelState.IsValid) 
{ 
_logger.LogError("Invalid model state for the EmployeeForUpdateDto object"); 
return UnprocessableEntity(ModelState); 
}

The same process can be applied to the Company DTO classes and create action. You can find it implemented in the source code for this chapter.
可以将相同的过程应用于公司 DTO 类并创建操作。您可以在本章的源代码中找到它的实现。

Let’s test this:
让我们测试一下:

{
    "name":null,
    "age":29,
    "position":null
}

https://localhost:5001/api/companies/53a1237a-3ed3-4462-b9f0-5a7bb1056a33/employees/80ABBCA8-664D-4B20-B5DE-024705497D4A

Alt text

Great.
伟大。

Everything works well.
一切正常。

13.3 Validation for PATCH Requests

The validation for PATCH requests is a bit different from the previous ones. We are using the ModelState concept again, but this time we have to place it in the ApplyTo method first:
PATCH 请求的验证与以前的验证略有不同。我们再次使用 ModelState 概念,但这次我们必须首先将其放在 ApplyTo 方法中:

patchDoc.ApplyTo(employeeToPatch, ModelState);

Right below, we can add our familiar validation logic:
在下面,我们可以添加我们熟悉的验证逻辑:


[HttpPatch("{id}")]
public IActionResult PartiallyUpdateEmployeeForCompany(Guid companyId, Guid id, [FromBody] JsonPatchDocument<EmployeeForUpdateDto> patchDoc)
{
    if (patchDoc == null)
    {
        _logger.LogError("patchDoc object sent from client is null.");
        return BadRequest("patchDoc object is null");
    }
    var company = _repository.Company.GetCompany(companyId, trackChanges: false);
    if (company == null)
    {
        _logger.LogInfo($"Company with id: {companyId} doesn't exist in the database.");
        return NotFound();
    }
    var employeeEntity = _repository.Employee.GetEmployee(companyId, id, trackChanges: true);
    if (employeeEntity == null)
    {
        _logger.LogInfo($"Employee with id: {id} doesn't exist in the database.");
        return NotFound();
    }
    var employeeToPatch = _mapper.Map<EmployeeForUpdateDto>(employeeEntity);
    patchDoc.ApplyTo(employeeToPatch,ModelState);
    if(!ModelState.IsValid)
    {
        _logger.LogError("Invalid model state for the patch document"); 
        return UnprocessableEntity(ModelState);
    }
    _mapper.Map(employeeToPatch, employeeEntity);
    _repository.Save();
    return NoContent();
}

Let’s test this now:
现在让我们测试一下:

https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees/80ABBCA8-664D-4B20-B5DE-024705497D4A

[
    {
        "op":"remove",
        "path":"/ageeeeeee"
    }
]

Alt text

You can see that it works as it is supposed to.
您可以看到它按预期工作。

But, we have a small problem now. What if we try to send a remove operation, but for the valid path:
但是,我们现在有一个小问题。如果我们尝试发送删除操作,但对于有效路径,该怎么办:

Alt text

We can see it passes, but this is not good. If you can remember, we said that the remove operation will set the value for the included property to its default value, which is 0. But in the EmployeeForUpdateDto class, we have a Range attribute which doesn’t allow that value to be below 18. So, where is the problem?
我们可以看到它过去了,但这并不好。如果您还记得,我们说过删除操作会将包含属性的值设置为其默认值,即 0。但是在 EmployeeForUpdateDto 类中,我们有一个 Range 属性,它不允许该值低于 18。那么,问题出在哪里呢?

Let’s illustrate this for you:
让我们为您说明这一点:
Alt text

As you can see, we are validating the patchDoc which is completely valid at this moment, but we save employeeEntity to the database. So, we need some additional validation to prevent an invalid employeeEntity from being saved to the database:
如您所见,我们正在验证目前完全有效的 patchDoc,但我们将 employeeEntity 保存到数据库中。因此,我们需要一些额外的验证来防止无效的 employeeEntity 保存到数据库中:

var employeeToPatch = _mapper.Map<EmployeeForUpdateDto>(employeeEntity); 
patchDoc.ApplyTo(employeeToPatch, ModelState);
TryValidateModel(employeeToPatch); 
if(!ModelState.IsValid) 
{
_logger.LogError("Invalid model state for the patch document"); 
return UnprocessableEntity(ModelState);
}

We can use the TryValidateModel method to validate the already patched employeeToPatch instance. This will trigger validation and every error will make ModelState invalid.
我们可以使用 TryValidateModel 方法来验证已经修补的 employeeToPatch 实例。这将触发验证,每个错误都会使模型状态无效。

After that, we execute a familiar validation check.
之后,我们执行熟悉的验证检查。

Now, we can test this again:
现在,我们可以再次测试:

[
    {
        "op":"remove",
        "path":"/age"
    }
]

https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees/80ABBCA8-664D-4B20-B5DE-024705497D4A

Alt text

And we get 422, which is the expected status code.
我们得到 422,这是预期的状态代码。

发表评论