{"id":1124,"date":"2025-05-27T14:46:51","date_gmt":"2025-05-27T06:46:51","guid":{"rendered":"https:\/\/www.hyy.net\/?p=1124"},"modified":"2025-05-27T14:46:51","modified_gmt":"2025-05-27T06:46:51","slug":"ultimate-asp-net-core-web-api-13-validation","status":"publish","type":"post","link":"https:\/\/diji.net\/?p=1124","title":{"rendered":"Ultimate ASP.NET Core Web API 13 VALIDATION"},"content":{"rendered":"<h1>13 VALIDATION<\/h1>\n<p>13 \u9a8c\u8bc1<\/p>\n<p>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:\u200c<br \/>\n\u5728\u7f16\u5199 API\u52a8\u4f5c\u65f6\uff0c\u6211\u4eec\u6709\u4e00\u7ec4\u89c4\u5219\u9700\u8981\u68c0\u67e5\u3002\u5982\u679c\u6211\u4eec\u770b\u4e00\u4e0b Company \u7c7b\uff0c\u6211\u4eec\u53ef\u4ee5\u5728\u5c5e\u6027\u4e0a\u65b9\u770b\u5230\u4e0d\u540c\u7684\u6570\u636e\u6ce8\u91ca\u5c5e\u6027\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1301.jpg\" alt=\"alt text\" \/><\/p>\n<p>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.<br \/>\n\u8fd9\u4e9b\u5c5e\u6027\u7528\u4e8e\u5728\u6570\u636e\u5e93\u4e2d\u521b\u5efa\u6216\u66f4\u65b0\u8d44\u6e90\u65f6\u9a8c\u8bc1\u6211\u4eec\u7684\u6a21\u578b\u5bf9\u8c61\u3002\u4f46\u6211\u4eec\u8fd8\u6ca1\u6709\u5229\u7528\u5b83\u4eec\u3002<\/p>\n<p>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.<br \/>\n\u5728\u672c\u7ae0\u4e2d\uff0c\u6211\u4eec\u5c06\u5411\u60a8\u5c55\u793a\u5982\u4f55\u9a8c\u8bc1\u6211\u4eec\u7684\u6a21\u578b\u5bf9\u8c61\uff0c\u4ee5\u53ca\u5982\u4f55\u5728\u6a21\u578b\u65e0\u6548\u65f6\u5411\u5ba2\u6237\u7aef\u8fd4\u56de\u9002\u5f53\u7684\u54cd\u5e94\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u9700\u8981\u9a8c\u8bc1\u63a7\u5236\u5668\u4f5c\u7684\u8f93\u5165\uff0c\u800c\u4e0d\u662f\u8f93\u51fa\u3002\u8fd9\u610f\u5473\u7740\u6211\u4eec\u5c06\u5c06\u6b64\u9a8c\u8bc1\u5e94\u7528\u4e8e POST\u3001PUT \u548c PATCH \u8bf7\u6c42\uff0c\u4f46\u4e0d\u5e94\u7528\u4e8e GET \u8bf7\u6c42\u3002<\/p>\n<h2>13.1 ModelState, Rerun Validation, and Built-in Attributes<\/h2>\n<p>13.1 ModelState\u3001rerun \u9a8c\u8bc1\u548c\u5185\u7f6e\u5c5e\u6027<\/p>\n<p>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.\u200c<br \/>\n\u4e3a\u4e86\u6839\u636e Data Annotation \u5c5e\u6027\u5e94\u7528\u7684\u9a8c\u8bc1\u89c4\u5219\u8fdb\u884c\u9a8c\u8bc1\uff0c\u6211\u4eec\u5c06\u4f7f\u7528 ModelState \u7684\u6982\u5ff5\u3002\u5b83\u662f\u4e00\u4e2a\u5305\u542b\u6a21\u578b\u72b6\u6001\u548c\u6a21\u578b\u7ed1\u5b9a\u9a8c\u8bc1\u7684\u5b57\u5178\u3002<\/p>\n<p>It is important to know that model validation occurs after model binding and reports errors where the data, sent from the client, doesn\u2019t meet our validation criteria. Both model validation and data binding occur before our request reaches an action inside a controller. We are going to use the ModelState.IsValid expression to check for those validation rules.<br \/>\n\u8bf7\u52a1\u5fc5\u4e86\u89e3\uff0c\u6a21\u578b\u9a8c\u8bc1\u53d1\u751f\u5728\u6a21\u578b\u7ed1\u5b9a\u4e4b\u540e\uff0c\u5e76\u62a5\u544a\u4ece\u5ba2\u6237\u7aef\u53d1\u9001\u7684\u6570\u636e\u4e0d\u7b26\u5408\u9a8c\u8bc1\u6761\u4ef6\u7684\u9519\u8bef\u3002\u6a21\u578b\u9a8c\u8bc1\u548c\u6570\u636e\u7ed1\u5b9a\u90fd\u53d1\u751f\u5728\u6211\u4eec\u7684\u8bf7\u6c42\u5230\u8fbe\u63a7\u5236\u5668\u5185\u7684 action \u4e4b\u524d\u3002\u6211\u4eec\u5c06\u4f7f\u7528 ModelState.IsValid \u8868\u8fbe\u5f0f\u6765\u68c0\u67e5\u8fd9\u4e9b\u9a8c\u8bc1\u89c4\u5219\u3002<\/p>\n<p>By default, we don\u2019t have to use the ModelState.IsValid expression in Web API projects since, as we explained in section 9.2.1, controllers are decorated with the [ApiController] attribute. But, as we could\u2019ve seen, it defaults all the model state errors to 400 \u2013 BadRequest and doesn\u2019t allow us to return our custom error messages with a different status code. So, we suppressed it in the Program class.<br \/>\n\u9ed8\u8ba4\u60c5\u51b5\u4e0b\uff0c\u6211\u4eec\u4e0d\u5fc5\u5728 Web API \u9879\u76ee\u4e2d\u4f7f\u7528 ModelState.IsValid \u8868\u8fbe\u5f0f\uff0c\u56e0\u4e3a\u6b63\u5982\u6211\u4eec\u5728\u7b2c 9.2.1 \u8282\u4e2d\u6240\u89e3\u91ca\u7684\u90a3\u6837\uff0c\u63a7\u5236\u5668\u662f\u4f7f\u7528 [ApiController] \u5c5e\u6027\u4fee\u9970\u7684\u3002\u4f46\u662f\uff0c\u6b63\u5982\u6211\u4eec\u6240\u770b\u5230\u7684\uff0c\u5b83\u9ed8\u8ba4\u6240\u6709\u6a21\u578b\u72b6\u6001\u9519\u8bef\u4e3a 400 \u2013 BadRequest\uff0c\u5e76\u4e14\u4e0d\u5141\u8bb8\u6211\u4eec\u8fd4\u56de\u5177\u6709\u4e0d\u540c\u72b6\u6001\u4ee3\u7801\u7684\u81ea\u5b9a\u4e49\u9519\u8bef\u6d88\u606f\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u5728 Program \u7c7b\u4e2d\u6291\u5236\u4e86\u5b83\u3002<\/p>\n<p>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. If we didn\u2019t suppress the model validation from the [ApiController] attribute, we wouldn\u2019t be able to return this status code (422) since, as we said, it would default to 400.<br \/>\n\u9a8c\u8bc1\u5931\u8d25\u65f6\uff0c\u54cd\u5e94\u72b6\u6001\u4ee3\u7801\u5e94\u4e3a 422 Unprocessable Entity\u3002\u8fd9\u610f\u5473\u7740\u670d\u52a1\u5668\u7406\u89e3\u8bf7\u6c42\u7684\u5185\u5bb9\u7c7b\u578b\uff0c\u5e76\u4e14\u8bf7\u6c42\u5b9e\u4f53\u7684\u8bed\u6cd5\u662f\u6b63\u786e\u7684\uff0c\u4f46\u5b83\u65e0\u6cd5\u5904\u7406\u5e94\u7528\u4e8e\u8bf7\u6c42\u6b63\u6587\u5185\u7684\u5b9e\u4f53\u7684\u9a8c\u8bc1\u89c4\u5219\u3002\u5982\u679c\u6211\u4eec\u6ca1\u6709\u4ece [ApiController] \u5c5e\u6027\u4e2d\u7981\u6b62\u663e\u793a\u6a21\u578b\u9a8c\u8bc1\uff0c\u6211\u4eec\u5c06\u65e0\u6cd5\u8fd4\u56de\u6b64\u72b6\u6001\u4ee3\u7801 \uff08422\uff09\uff0c\u56e0\u4e3a\u6b63\u5982\u6211\u4eec\u6240\u8bf4\uff0c\u5b83\u5c06\u9ed8\u8ba4\u4e3a 400\u3002<\/p>\n<h2>13.1.1 Rerun Validation\u200c<\/h2>\n<p>13.1.1 \u91cd\u65b0\u8fd0\u884c\u9a8c\u8bc1<\/p>\n<p>In some cases, we want to repeat our validation. This can happen if, after the initial validation, we compute a value in our code, and assign it to the property of an already validated object.<br \/>\n\u5728\u67d0\u4e9b\u60c5\u51b5\u4e0b\uff0c\u6211\u4eec\u5e0c\u671b\u91cd\u590d\u9a8c\u8bc1\u3002\u5982\u679c\u5728\u521d\u59cb\u9a8c\u8bc1\u4e4b\u540e\uff0c\u6211\u4eec\u5728\u4ee3\u7801\u4e2d\u8ba1\u7b97\u4e00\u4e2a\u503c\uff0c\u5e76\u5c06\u5176\u5206\u914d\u7ed9\u5df2\u9a8c\u8bc1\u5bf9\u8c61\u7684\u5c5e\u6027\uff0c\u5219\u53ef\u80fd\u4f1a\u53d1\u751f\u8fd9\u79cd\u60c5\u51b5\u3002<\/p>\n<p>If this is the case, and we want to run the validation again, we can use the ModelStateDictionary.ClearValidationState method to clear the validation specific to the model that we\u2019ve already validated, and then use the TryValidateModel method:<br \/>\n\u5982\u679c\u662f\u8fd9\u79cd\u60c5\u51b5\uff0c\u5e76\u4e14\u6211\u4eec\u60f3\u8981\u518d\u6b21\u8fd0\u884c\u9a8c\u8bc1\uff0c\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528 ModelStateDictionary.ClearValidationState \u65b9\u6cd5\u6765\u6e05\u9664\u7279\u5b9a\u4e8e\u6211\u4eec\u5df2\u7ecf\u9a8c\u8bc1\u7684\u6a21\u578b\u7684\u9a8c\u8bc1\uff0c\u7136\u540e\u4f7f\u7528 TryValidateModel \u65b9\u6cd5\uff1a<\/p>\n<pre><code>[HttpPost]\npublic IActionResult POST([FromBody] Book book)\n{\n    if (!ModelState.IsValid)\n        return UnprocessableEntity(ModelState);\n    var newPrice = book.Price - 10;\n    book.Price = newPrice;\n    ModelState.ClearValidationState(nameof(Book));\n    if (!TryValidateModel(book, nameof(Book)))\n        return UnprocessableEntity(ModelState);\n    _service.CreateBook(book);\n    return CreatedAtRoute(&quot;BookById&quot;, new { id = book.Id }, book);\n}<\/code><\/pre>\n<p>This is just a simple example but it explains how we can revalidate our model object.<br \/>\n\u8fd9\u53ea\u662f\u4e00\u4e2a\u7b80\u5355\u7684\u793a\u4f8b\uff0c\u4f46\u5b83\u89e3\u91ca\u4e86\u6211\u4eec\u5982\u4f55\u91cd\u65b0\u9a8c\u8bc1\u6211\u4eec\u7684\u6a21\u578b\u5bf9\u8c61\u3002<\/p>\n<h3>13.1.2 Built-in Attributes\u200c<\/h3>\n<p>13.1.2 \u5185\u7f6e\u5c5e\u6027<\/p>\n<p>Validation attributes let us specify validation rules for model properties. At the beginning of this chapter, we have marked some validation attributes. Those attributes (Required and MaxLength) are part of built-in attributes. And of course, there are more than two built-in attributes. These are the most used ones:<br \/>\n\u9a8c\u8bc1\u5c5e\u6027\u5141\u8bb8\u6211\u4eec\u4e3a\u6a21\u578b\u5c5e\u6027\u6307\u5b9a\u9a8c\u8bc1\u89c4\u5219\u3002\u5728\u672c\u7ae0\u7684\u5f00\u5934\uff0c\u6211\u4eec\u6807\u8bb0\u4e86\u4e00\u4e9b\u9a8c\u8bc1\u5c5e\u6027\u3002\u8fd9\u4e9b\u5c5e\u6027 \uff08Required \u548c MaxLength\uff09 \u662f\u5185\u7f6e\u5c5e\u6027\u7684\u4e00\u90e8\u5206\u3002\u5f53\u7136\uff0c\u8fd8\u6709\u4e0d\u6b62\u4e24\u4e2a\u5185\u7f6e\u5c5e\u6027\u3002\u8fd9\u4e9b\u662f\u6700\u5e38\u7528\u7684\uff1a<\/p>\n<table>\n<thead>\n<tr>\n<th>ATTRIBUTE<\/th>\n<th>USAGE<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>[ValidateNever]<\/td>\n<td>Indicates that property or parameter should be excluded from validation.<\/td>\n<\/tr>\n<tr>\n<td>[Compare]<\/td>\n<td>We use it for the properties comparison.<\/td>\n<\/tr>\n<tr>\n<td>[EmailAddress]<\/td>\n<td>Validates the email format of the property.<\/td>\n<\/tr>\n<tr>\n<td>[Phone]<\/td>\n<td>Validates the phone format of the property.<\/td>\n<\/tr>\n<tr>\n<td>[Range]<\/td>\n<td>Validates that the property falls within a specified range.<\/td>\n<\/tr>\n<tr>\n<td>[RegularExpression]<\/td>\n<td>Validates that the property value matches a specified regular expression.<\/td>\n<\/tr>\n<tr>\n<td>[Required]<\/td>\n<td>We use it to prevent a null value for the property.<\/td>\n<\/tr>\n<tr>\n<td>[StringLength]<\/td>\n<td>Validates that a string property value doesn't exceed a specified length limit.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>If you want to see a complete list of built-in attributes, you can visit this page. <a href=\"https:\/\/learn.microsoft.com\/en-us\/dotnet\/api\/system.componentmodel.dataannotations?view=net-6.0\">https:\/\/learn.microsoft.com\/en-us\/dotnet\/api\/system.componentmodel.dataannotations?view=net-6.0<\/a><br \/>\n\u5982\u679c\u60a8\u60f3\u67e5\u770b\u5185\u7f6e\u5c5e\u6027\u7684\u5b8c\u6574\u5217\u8868\uff0c\u53ef\u4ee5\u8bbf\u95ee\u6b64\u9875\u9762\u3002<a href=\"https:\/\/learn.microsoft.com\/en-us\/dotnet\/api\/system.componentmodel.dataannotations?view=net-6.0\">https:\/\/learn.microsoft.com\/en-us\/dotnet\/api\/system.componentmodel.dataannotations?view=net-6.0<\/a><\/p>\n<h2>13.2 Custom Attributes and IValidatableObject<\/h2>\n<p>13.2 \u81ea\u5b9a\u4e49\u5c5e\u6027\u548c IValidatableObject<\/p>\n<p>There are scenarios where built-in attributes are not enough and we have to provide some custom logic. For that, we can create a custom attribute by using the ValidationAttribute class, or we can use the IValidatableObject interface.\u200c<br \/>\n\u5728\u67d0\u4e9b\u60c5\u51b5\u4e0b\uff0c\u5185\u7f6e\u5c5e\u6027\u662f\u4e0d\u591f\u7684\uff0c\u6211\u4eec\u5fc5\u987b\u63d0\u4f9b\u4e00\u4e9b\u81ea\u5b9a\u4e49\u903b\u8f91\u3002\u4e3a\u6b64\uff0c\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528 ValidationAttribute \u7c7b\u521b\u5efa\u81ea\u5b9a\u4e49\u5c5e\u6027\uff0c\u4e5f\u53ef\u4ee5\u4f7f\u7528 IValidatableObject \u63a5\u53e3\u3002<\/p>\n<p>So, let\u2019s see an example of how we can create a custom attribute:<br \/>\n\u90a3\u4e48\uff0c\u8ba9\u6211\u4eec\u770b\u770b\u5982\u4f55\u521b\u5efa\u81ea\u5b9a\u4e49\u5c5e\u6027\u7684\u793a\u4f8b\uff1a<\/p>\n<pre><code>public class ScienceBookAttribute : ValidationAttribute\n{\n    public BookGenre Genre { get; set; }\n    public string Error =&gt; $&quot;The genre of the book must be {BookGenre.Science}&quot;;\n    public ScienceBookAttribute(BookGenre genre) { Genre = genre; }\n    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)\n    {\n        var book = (Book)validationContext.ObjectInstance;\n        if (!book.Genre.Equals(Genre.ToString()))\n            return new ValidationResult(Error);\n        return ValidationResult.Success;\n    }\n}<\/code><\/pre>\n<p>Once this attribute is called, we are going to pass the genre parameter inside the constructor. Then, we have to override the IsValid method. There we extract the object we want to validate and inspect if the Genre property matches our value sent through the constructor. If it\u2019s not we return the Error property as a validation result. Otherwise, we return success.<br \/>\n\u8c03\u7528\u6b64\u5c5e\u6027\u540e\uff0c\u6211\u4eec\u5c06\u5728\u6784\u9020\u51fd\u6570\u4e2d\u4f20\u9012 genre \u53c2\u6570\u3002\u7136\u540e\uff0c\u6211\u4eec\u5fc5\u987b\u91cd\u5199 IsValid \u65b9\u6cd5\u3002\u5728\u90a3\u91cc\uff0c\u6211\u4eec\u63d0\u53d6\u8981\u9a8c\u8bc1\u7684\u5bf9\u8c61\uff0c\u5e76\u68c0\u67e5 Genre \u5c5e\u6027\u662f\u5426\u4e0e\u901a\u8fc7\u6784\u9020\u51fd\u6570\u53d1\u9001\u7684\u503c\u5339\u914d\u3002\u5982\u679c\u4e0d\u662f\uff0c\u6211\u4eec\u5c06\u8fd4\u56de Error \u5c5e\u6027\u4f5c\u4e3a\u9a8c\u8bc1\u7ed3\u679c\u3002\u5426\u5219\uff0c\u6211\u4eec\u5c06\u8fd4\u56de success\u3002<\/p>\n<p>To call this custom attribute, we can do something like this:<br \/>\n\u8981\u8c03\u7528\u8fd9\u4e2a custom attribute\uff0c\u6211\u4eec\u53ef\u4ee5\u505a\u8fd9\u6837\u7684\u4e8b\u60c5\uff1a<\/p>\n<pre><code>public class Book\n{\n    public int Id { get; set; }\n    [Required] public string? Name { get; set; }\n    [Range(10, int.MaxValue)] public int Price { get; set; }\n    [ScienceBook(BookGenre.Science)] public string? Genre { get; set; }\n}<\/code><\/pre>\n<p>Now we can use the IValidatableObject interface:<br \/>\n\u73b0\u5728\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528 IValidatableObject \u63a5\u53e3\uff1a<\/p>\n<pre><code>public class Book : IValidatableObject\n{\n    public int Id { get; set; }\n    [Required] public string? Name { get; set; }\n    [Range(10, int.MaxValue)] public int Price { get; set; }\n    public string? Genre { get; set; }\n    public IEnumerable&lt;ValidationResult&gt; Validate(ValidationContext validationContext)\n    {\n        var errorMessage = $&quot;The genre of the book must be {BookGenre.Science}&quot;;\n        if (!Genre.Equals(BookGenre.Science.ToString()))\n            yield return new ValidationResult(errorMessage, new[] { nameof(Genre) });\n    }\n}<\/code><\/pre>\n<p>This validation happens in the model class, where we have to implement the Validate method. The code inside that method is pretty straightforward. Also, pay attention that we don\u2019t have to apply any validation attribute on top of the Genre property.<br \/>\n\u6b64\u9a8c\u8bc1\u53d1\u751f\u5728 model \u7c7b\u4e2d\uff0c\u6211\u4eec\u5fc5\u987b\u5728\u5176\u4e2d\u5b9e\u73b0 Validate \u65b9\u6cd5\u3002\u8be5\u65b9\u6cd5\u4e2d\u7684\u4ee3\u7801\u975e\u5e38\u7b80\u5355\u3002\u6b64\u5916\uff0c\u8bf7\u6ce8\u610f\uff0c\u6211\u4eec\u4e0d\u5fc5\u5728 Genre \u5c5e\u6027\u4e4b\u4e0a\u5e94\u7528\u4efb\u4f55 validation \u5c5e\u6027\u3002<\/p>\n<p>As we\u2019ve seen from the previous examples, we can create a custom attribute in a separate class and even make it generic so it could be reused for other model objects. This is not the case with the IValidatableObject interface. It is used inside the model class and of course, the validation logic can\u2019t be reused.<br \/>\n\u6b63\u5982\u6211\u4eec\u4ece\u524d\u9762\u7684\u793a\u4f8b\u4e2d\u770b\u5230\u7684\uff0c\u6211\u4eec\u53ef\u4ee5\u5728\u5355\u72ec\u7684\u7c7b\u4e2d\u521b\u5efa\u81ea\u5b9a\u4e49\u5c5e\u6027\uff0c\u751a\u81f3\u53ef\u4ee5\u5c06\u5176\u8bbe\u7f6e\u4e3a\u6cdb\u578b\uff0c\u4ee5\u4fbf\u5b83\u53ef\u4ee5\u88ab\u5176\u4ed6\u6a21\u578b\u5bf9\u8c61\u91cd\u7528\u3002IValidatableObject \u63a5\u53e3\u5e76\u975e\u5982\u6b64\u3002\u5b83\u5728 model \u7c7b\u4e2d\u4f7f\u7528\uff0c\u5f53\u7136\uff0c\u9a8c\u8bc1\u903b\u8f91\u4e0d\u80fd\u91cd\u7528\u3002<\/p>\n<p>So, this could be something you can think about when deciding which one to use.<br \/>\n\u56e0\u6b64\uff0c\u8fd9\u53ef\u80fd\u662f\u60a8\u5728\u51b3\u5b9a\u4f7f\u7528\u54ea\u4e00\u4e2a\u65f6\u53ef\u4ee5\u8003\u8651\u7684\u4e8b\u60c5\u3002<\/p>\n<p>After all of this theory and code samples, we are ready to implement model validation in our code.<br \/>\n\u5728\u6240\u6709\u8fd9\u4e9b\u7406\u8bba\u548c\u4ee3\u7801\u793a\u4f8b\u4e4b\u540e\uff0c\u6211\u4eec\u51c6\u5907\u597d\u5728\u4ee3\u7801\u4e2d\u5b9e\u73b0\u6a21\u578b\u9a8c\u8bc1\u3002<\/p>\n<h2>13.3 Validation while Creating Resource<\/h2>\n<p>13.3 \u521b\u5efa\u8d44\u6e90\u65f6\u8fdb\u884c\u9a8c\u8bc1<\/p>\n<p>Let\u2019s send another request for the CreateEmployee action, but this time with the invalid request body:\u200c<br \/>\n\u8ba9\u6211\u4eec\u4e3a CreateEmployee\u4f5c\u53d1\u9001\u53e6\u4e00\u4e2a\u8bf7\u6c42\uff0c\u4f46\u8fd9\u6b21\u7684\u8bf7\u6c42\u6b63\u6587\u65e0\u6548\uff1a<br \/>\n<a href=\"https:\/\/localhost:5001\/api\/companies\/582ea192-6fb7-44ff-a2a1-08d988ca3ca9\/employees\">https:\/\/localhost:5001\/api\/companies\/582ea192-6fb7-44ff-a2a1-08d988ca3ca9\/employees<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1302.jpg\" alt=\"alt text\" \/><\/p>\n<p>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 the wrong model to the API \u2014 thus the error message should be different.<br \/>\n\u6211\u4eec\u5f97\u5230 500 Internal Server Error\uff0c\u8fd9\u662f\u6211\u4eec\u7684\u4ee3\u7801\u4e2d\u53d1\u751f\u672a\u5904\u7406\u7684\u4e8b\u60c5\u65f6\u7684\u4e00\u822c\u6d88\u606f\u3002\u4f46\u8fd9\u5e76\u4e0d\u597d\u3002\u8fd9\u610f\u5473\u7740\u670d\u52a1\u5668\u51fa\u9519\uff0c\u4f46\u4e8b\u5b9e\u5e76\u975e\u5982\u6b64\u3002\u5728\u8fd9\u79cd\u60c5\u51b5\u4e0b\uff0c\u6211\u4eec\u4f5c\u4e3a\u6d88\u8d39\u8005\u5411 API \u53d1\u9001\u4e86\u9519\u8bef\u7684\u6a21\u578b\u2014\u2014\u56e0\u6b64\u9519\u8bef\u6d88\u606f\u5e94\u8be5\u4e0d\u540c\u3002<\/p>\n<p>To fix this, let\u2019s modify our EmployeeForCreationDto record because that\u2019s what we deserialize the request body to:<br \/>\n\u4e3a\u4e86\u89e3\u51b3\u8fd9\u4e2a\u95ee\u9898\uff0c\u8ba9\u6211\u4eec\u4fee\u6539\u6211\u4eec\u7684 EmployeeForCreationDto \u8bb0\u5f55\uff0c\u56e0\u4e3a\u8fd9\u662f\u6211\u4eec\u5c06\u8bf7\u6c42\u6b63\u6587\u53cd\u5e8f\u5217\u5316\u4e3a\uff1a<\/p>\n<pre><code>using System.ComponentModel.DataAnnotations;\n\nnamespace Shared.DataTransferObjects\n{\n    public record EmployeeForCreationDto(\n        [Required(ErrorMessage = &quot;Employee name is a required field.&quot;)]\n        [MaxLength(30, ErrorMessage = &quot;Maximum length for the Name is 30 characters.&quot;)]\n        string Name,\n\n        [Required(ErrorMessage = &quot;Age is a required field.&quot;)]\n        int Age,\n\n        [Required(ErrorMessage = &quot;Position is a required field.&quot;)]\n        [MaxLength(20, ErrorMessage = &quot;Maximum length for the Position is 20 characters.&quot;)]\n\n        string Position\n        );\n}<\/code><\/pre>\n<p>This is how we can apply validation attributes in our positional records. But, in our opinion, positional records start losing readability once the attributes are applied, and for that reason, we like using init setters if we have to apply validation attributes. So, we are going to do exactly that and modify this position record:<br \/>\n\u8fd9\u5c31\u662f\u6211\u4eec\u5728\u4f4d\u7f6e\u8bb0\u5f55\u4e2d\u5e94\u7528\u9a8c\u8bc1\u5c5e\u6027\u7684\u65b9\u6cd5\u3002\u4f46\u662f\uff0c\u5728\u6211\u4eec\u770b\u6765\uff0c\u4e00\u65e6\u5e94\u7528\u4e86\u5c5e\u6027\uff0c\u4f4d\u7f6e\u8bb0\u5f55\u5c31\u4f1a\u5f00\u59cb\u5931\u53bb\u53ef\u8bfb\u6027\uff0c\u56e0\u6b64\uff0c\u5982\u679c\u5fc5\u987b\u5e94\u7528\u9a8c\u8bc1\u5c5e\u6027\uff0c\u6211\u4eec\u559c\u6b22\u4f7f\u7528 init setter\u3002\u6240\u4ee5\uff0c\u6211\u4eec\u5c06\u8fd9\u6837\u505a\u5e76\u4fee\u6539\u8fd9\u4e2a\u4f4d\u7f6e\u8bb0\u5f55\uff1a<\/p>\n<pre><code>using System.ComponentModel.DataAnnotations;\n\nnamespace Shared.DataTransferObjects\n{\n    public record EmployeeForCreationDto\n    {\n        [Required(ErrorMessage = &quot;Employee name is a required field.&quot;)]\n        [MaxLength(30, ErrorMessage = &quot;Maximum length for the Name is 30 characters.&quot;)]\n        public string? Name { get; init; }\n\n        [Required(ErrorMessage = &quot;Age is a required field.&quot;)]\n        public int Age { get; init; }\n\n        [Required(ErrorMessage = &quot;Position is a required field.&quot;)]\n        [MaxLength(20, ErrorMessage = &quot;Maximum length for the Position is 20 characters.&quot;)]\n        public string? Position { get; init; }\n    }\n}<\/code><\/pre>\n<p>Now, we have to modify our action:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539\u6211\u4eec\u7684\u4f5c\uff1a<\/p>\n<pre><code>\/\/[HttpPost]\n\/\/public IActionResult CreateEmployeeForCompany(Guid companyId, [FromBody] EmployeeForCreationDto employee)\n\/\/{\n\/\/    if (employee is null)\n\/\/        return BadRequest(&quot;EmployeeForCreationDto object is null&quot;);\n\/\/    var employeeToReturn = _service.EmployeeService.CreateEmployeeForCompany(companyId, employee, trackChanges: false);\n\/\/    return CreatedAtRoute(&quot;GetEmployeeForCompany&quot;, new { companyId, id = employeeToReturn.Id }, employeeToReturn);\n\/\/}\n\n[HttpPost]\npublic IActionResult CreateEmployeeForCompany(Guid companyId, [FromBody] EmployeeForCreationDto employee)\n{\n    if (employee is null) return BadRequest(&quot;EmployeeForCreationDto object is null&quot;);\n    if (!ModelState.IsValid) return UnprocessableEntity(ModelState);\n    var employeeToReturn = _service.EmployeeService.CreateEmployeeForCompany(companyId, employee, trackChanges: false);\n    return CreatedAtRoute(&quot;GetEmployeeForCompany&quot;, new { companyId, id = employeeToReturn.Id }, employeeToReturn);\n}<\/code><\/pre>\n<p>As mentioned before in the part about the ModelState dictionary, all we have to do is to call the IsValid method and return the UnprocessableEntity response by providing our ModelState.<br \/>\n\u5982\u524d\u6240\u8ff0\uff0c\u5728\u524d\u9762\u5173\u4e8e ModelState \u5b57\u5178\u7684\u90e8\u5206\uff0c\u6211\u4eec\u6240\u8981\u505a\u7684\u5c31\u662f\u8c03\u7528 IsValid \u65b9\u6cd5\uff0c\u5e76\u901a\u8fc7\u63d0\u4f9b ModelState \u8fd4\u56de UnprocessableEntity \u54cd\u5e94\u3002<\/p>\n<p>And that is all.<br \/>\n\u5c31\u8fd9\u6837\u3002<\/p>\n<p>Let\u2019s send our request one more time:<br \/>\n\u8ba9\u6211\u4eec\u518d\u53d1\u9001\u4e00\u6b21\u6211\u4eec\u7684\u8bf7\u6c42\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/582ea192-6fb7-44ff-a2a1-08d988ca3ca9\/employees\">https:\/\/localhost:5001\/api\/companies\/582ea192-6fb7-44ff-a2a1-08d988ca3ca9\/employees<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1303.jpg\" alt=\"alt text\" \/><\/p>\n<p>Let\u2019s send an additional request to test the max length rule:<br \/>\n\u8ba9\u6211\u4eec\u53d1\u9001\u4e00\u4e2a\u989d\u5916\u7684\u8bf7\u6c42\u6765\u6d4b\u8bd5 max length \u89c4\u5219\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/582ea192-6fb7-44ff-a2a1-08d988ca3ca9\/employees\">https:\/\/localhost:5001\/api\/companies\/582ea192-6fb7-44ff-a2a1-08d988ca3ca9\/employees<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1304.jpg\" alt=\"alt text\" \/><\/p>\n<p>Excellent. It works as expected.<br \/>\n\u975e\u5e38\u597d\u3002\u5b83\u6309\u9884\u671f\u5de5\u4f5c\u3002<\/p>\n<p>The same actions can be applied for the CreateCompany action and CompanyForCreationDto class \u2014 and if you check the source code for this chapter, you will find it implemented.<br \/>\n\u76f8\u540c\u7684\u4f5c\u53ef\u4ee5\u5e94\u7528\u4e8e CreateCompany\u4f5c\u548c CompanyForCreationDto \u7c7b \u2014 \u5982\u679c\u60a8\u67e5\u770b\u672c\u7ae0\u7684\u6e90\u4ee3\u7801\uff0c\u60a8\u4f1a\u53d1\u73b0\u5b83\u5df2\u5b9e\u73b0\u3002<\/p>\n<h2>13.3.1 Validating Int Type\u200c<\/h2>\n<p>13.3.1 \u9a8c\u8bc1 int \u7c7b\u578b<\/p>\n<p>Let\u2019s create one more request with the request body without the age property:<br \/>\n\u8ba9\u6211\u4eec\u518d\u521b\u5efa\u4e00\u4e2a\u8bf7\u6c42\uff0c\u8bf7\u6c42\u6b63\u6587\u4e0d\u5e26 age \u5c5e\u6027\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/582ea192-6fb7-44ff-a2a1-08d988ca3ca9\/employees\">https:\/\/localhost:5001\/api\/companies\/582ea192-6fb7-44ff-a2a1-08d988ca3ca9\/employees<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1305.jpg\" alt=\"alt text\" \/><\/p>\n<p>We can see that the age property hasn\u2019t been sent, but in the response body, we don\u2019t 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\u2019t send that property, it would be set to a default value, which is 0.<br \/>\n\u6211\u4eec\u53ef\u4ee5\u770b\u5230 age \u5c5e\u6027\u5c1a\u672a\u53d1\u9001\uff0c\u4f46\u5728\u54cd\u5e94\u6b63\u6587\u4e2d\uff0c\u6211\u4eec\u6ca1\u6709\u5728\u5176\u4ed6\u9519\u8bef\u6d88\u606f\u65c1\u8fb9\u770b\u5230 age \u5c5e\u6027\u7684\u9519\u8bef\u6d88\u606f\u3002\u8fd9\u662f\u56e0\u4e3a age \u662f int \u7c7b\u578b\uff0c\u5982\u679c\u6211\u4eec\u4e0d\u53d1\u9001\u8be5\u5c5e\u6027\uff0c\u5b83\u5c06\u88ab\u8bbe\u7f6e\u4e3a\u9ed8\u8ba4\u503c\uff0c\u5373 0\u3002<\/p>\n<p>So, on the server-side, validation for the Age property will pass, because it is not null.<br \/>\n\u56e0\u6b64\uff0c\u5728\u670d\u52a1\u5668\u7aef\uff0c\u5bf9 Age \u5c5e\u6027\u7684\u9a8c\u8bc1\u5c06\u901a\u8fc7\uff0c\u56e0\u4e3a\u5b83\u4e0d\u662f null\u3002<\/p>\n<p>To prevent this type of behavior, we have to modify the data annotation attribute on top of the Age property in the EmployeeForCreationDto class:<br \/>\n\u4e3a\u4e86\u9632\u6b62\u6b64\u7c7b\u884c\u4e3a\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539 EmployeeForCreationDto \u7c7b\u4e2d Age \u5c5e\u6027\u9876\u90e8\u7684\u6570\u636e\u6ce8\u91ca\u5c5e\u6027\uff1a<\/p>\n<pre><code>[Range(18, int.MaxValue, ErrorMessage = &quot;Age is required and it can&#039;t be lower than 18&quot;)]\npublic int Age { get; init; }<\/code><\/pre>\n<p>Now, let\u2019s try to send the same request one more time:<br \/>\n\u73b0\u5728\uff0c\u8ba9\u6211\u4eec\u5c1d\u8bd5\u518d\u6b21\u53d1\u9001\u76f8\u540c\u7684\u8bf7\u6c42\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/582ea192-6fb7-44ff-a2a1-08d988ca3ca9\/employees\">https:\/\/localhost:5001\/api\/companies\/582ea192-6fb7-44ff-a2a1-08d988ca3ca9\/employees<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1306.jpg\" alt=\"alt text\" \/><\/p>\n<p>Now, we have the Age error message in our response.<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u7684\u54cd\u5e94\u4e2d\u6709 Age \u9519\u8bef\u6d88\u606f\u3002<\/p>\n<p>If we want, we can add the custom error messages in our action: ModelState.AddModelError(string key, string errorMessage)<br \/>\n\u5982\u679c\u9700\u8981\uff0c\u6211\u4eec\u53ef\u4ee5\u5728\u4f5c\u4e2d\u6dfb\u52a0\u81ea\u5b9a\u4e49\u9519\u8bef\u6d88\u606f\uff1aModelState.AddModelError\uff08string key\uff0c string errorMessage\uff09<\/p>\n<p>With this expression, the additional error message will be included with all the other messages.<br \/>\n\u4f7f\u7528\u6b64\u8868\u8fbe\u5f0f\uff0c\u5176\u4ed6\u9519\u8bef\u6d88\u606f\u5c06\u5305\u542b\u5728\u6240\u6709\u5176\u4ed6\u6d88\u606f\u4e2d\u3002<\/p>\n<h2>13.4 Validation for PUT Requests<\/h2>\n<p>13.4 PUT \u8bf7\u6c42\u7684\u9a8c\u8bc1<\/p>\n<p>The validation for PUT requests shouldn\u2019t be different from POST requests (except in some cases), but there are still things we have to do to at least optimize our code.\u200c<br \/>\nPUT \u8bf7\u6c42\u7684\u9a8c\u8bc1\u4e0d\u5e94\u4e0e POST \u8bf7\u6c42\u4e0d\u540c\uff08\u5728\u67d0\u4e9b\u60c5\u51b5\u4e0b\u9664\u5916\uff09\uff0c\u4f46\u6211\u4eec\u4ecd\u7136\u9700\u8981\u505a\u4e00\u4e9b\u4e8b\u60c5\u6765\u81f3\u5c11\u4f18\u5316\u6211\u4eec\u7684\u4ee3\u7801\u3002<\/p>\n<p>But let\u2019s go step by step.<br \/>\n\u4f46\u662f\uff0c\u8ba9\u6211\u4eec\u4e00\u6b65\u4e00\u6b65\u6765\u3002<\/p>\n<p>First, let\u2019s add Data Annotation Attributes to the EmployeeForUpdateDto record:<br \/>\n\u9996\u5148\uff0c\u8ba9\u6211\u4eec\u5c06 Data Annotation Attributes \u6dfb\u52a0\u5230 EmployeeForUpdateDto \u8bb0\u5f55\u4e2d\uff1a<\/p>\n<pre><code>using System.ComponentModel.DataAnnotations;\n\nnamespace Shared.DataTransferObjects\n{\n    public record EmployeeForUpdateDto\n    {\n        [Required(ErrorMessage = &quot;Employee name is a required field.&quot;)]\n        [MaxLength(30, ErrorMessage = &quot;Maximum length for the Name is 30 characters.&quot;)]\n\n        public string? Name { get; init; }\n\n        [Range(18, int.MaxValue, ErrorMessage = &quot;Age is required and it can&#039;t be lower than 18&quot;)]\n        public int Age { get; init; }\n\n        [Required(ErrorMessage = &quot;Position is a required field.&quot;)]\n        [MaxLength(20, ErrorMessage = &quot;Maximum length for the Position is 20 characters.&quot;)]\n        public string? Position { get; init; }\n    }\n}<\/code><\/pre>\n<p>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\u2019t want to repeat ourselves, thus we are going to add some modifications.<br \/>\n\u4e00\u65e6\u6211\u4eec\u5b8c\u6210\u4e86\u8fd9\u4e9b\uff0c\u6211\u4eec\u5c31\u4f1a\u610f\u8bc6\u5230\u6211\u4eec\u6709\u4e00\u4e2a\u5c0f\u95ee\u9898\u3002\u5982\u679c\u6211\u4eec\u5c06\u8fd9\u4e2a\u7c7b\u4e0e\u7528\u4e8e\u521b\u5efa\u7684 DTO \u7c7b\u8fdb\u884c\u6bd4\u8f83\uff0c\u6211\u4eec\u5c06\u53d1\u73b0\u5b83\u4eec\u662f\u76f8\u540c\u7684\u3002\u5f53\u7136\uff0c\u6211\u4eec\u4e0d\u60f3\u91cd\u590d\u81ea\u5df1\uff0c\u56e0\u6b64\u6211\u4eec\u5c06\u6dfb\u52a0\u4e00\u4e9b\u4fee\u6539\u3002<\/p>\n<p>Let\u2019s create a new record in the DataTransferObjects folder:<br \/>\n\u8ba9\u6211\u4eec\u5728 DataTransferObjects \u6587\u4ef6\u5939\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u8bb0\u5f55\uff1a<\/p>\n<pre><code>using System.ComponentModel.DataAnnotations;\n\nnamespace Shared.DataTransferObjects\n{\n    public abstract record EmployeeForManipulationDto\n    {\n        [Required(ErrorMessage = &quot;Employee name is a required field.&quot;)]\n        [MaxLength(30, ErrorMessage = &quot;Maximum length for the Name is 30 characters.&quot;)]\n        public string? Name { get; init; }\n\n        [Range(18, int.MaxValue, ErrorMessage = &quot;Age is required and it can&#039;t be lower than 18&quot;)]\n        public int Age { get; init; }\n\n        [Required(ErrorMessage = &quot;Position is a required field.&quot;)]\n        [MaxLength(20, ErrorMessage = &quot;Maximum length for the Position is 20 characters.&quot;)]\n        public string? Position { get; init; }\n    }\n}<\/code><\/pre>\n<p>We create this record as an abstract record because we want our creation and update DTO records to inherit from it:<br \/>\n\u6211\u4eec\u5c06\u6b64\u8bb0\u5f55\u521b\u5efa\u4e3a\u62bd\u8c61\u8bb0\u5f55\uff0c\u56e0\u4e3a\u6211\u4eec\u5e0c\u671b\u521b\u5efa\u548c\u66f4\u65b0 DTO \u8bb0\u5f55\u7ee7\u627f\u81ea\u5b83\uff1a<\/p>\n<pre><code>public record EmployeeForCreationDto : EmployeeForManipulationDto; \npublic record EmployeeForUpdateDto : EmployeeForManipulationDto;<\/code><\/pre>\n<p>Now, we can modify the UpdateEmployeeForCompany action by adding the model validation right after the null check:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u5728 null \u68c0\u67e5\u540e\u7acb\u5373\u6dfb\u52a0\u6a21\u578b\u9a8c\u8bc1\u6765\u4fee\u6539 UpdateEmployeeForCompany\u4f5c\uff1a<\/p>\n<pre><code>if (employee is null) return BadRequest(&quot;EmployeeForUpdateDto object is null&quot;); \nif (!ModelState.IsValid) return UnprocessableEntity(ModelState);<\/code><\/pre>\n<p>The same process can be applied to the Company DTO records and actions. You can find it implemented in the source code for this chapter.<br \/>\n\u76f8\u540c\u7684\u8fc7\u7a0b\u53ef\u4ee5\u5e94\u7528\u4e8e\u516c\u53f8 DTO \u8bb0\u5f55\u548c\u4f5c\u3002\u60a8\u53ef\u4ee5\u5728\u672c\u7ae0\u7684\u6e90\u4ee3\u7801\u4e2d\u627e\u5230\u5b83\u5b9e\u73b0\u7684\u3002<\/p>\n<p>Let\u2019s test this:<br \/>\n\u8ba9\u6211\u4eec\u6d4b\u8bd5\u4e00\u4e0b\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78-2D54A9991870\/employees\/80ABBCA8-664D-4B20-B5DE-024705497D4A\">https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78-2D54A9991870\/employees\/80ABBCA8-664D-4B20-B5DE-024705497D4A<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1307.jpg\" alt=\"alt text\" \/><\/p>\n<p>Great.Everything works well.<br \/>\n\u4e00\u5207\u90fd\u5f88\u987a\u5229\u3002<\/p>\n<h2>13.5 Validation for PATCH Requests<\/h2>\n<p>13.5 PATCH \u8bf7\u6c42\u7684\u9a8c\u8bc1<\/p>\n<p>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:\u200c<br \/>\nPATCH \u8bf7\u6c42\u7684\u9a8c\u8bc1\u4e0e\u524d\u9762\u7684\u9a8c\u8bc1\u7565\u6709\u4e0d\u540c\u3002\u6211\u4eec\u518d\u6b21\u4f7f\u7528 ModelState \u6982\u5ff5\uff0c\u4f46\u8fd9\u6b21\u6211\u4eec\u5fc5\u987b\u9996\u5148\u5c06\u5176\u653e\u5728 ApplyTo \u65b9\u6cd5\u4e2d\uff1a<\/p>\n<pre><code>patchDoc.ApplyTo(employeeToPatch, ModelState);<\/code><\/pre>\n<p>But once we do this, we are going to get an error. That\u2019s because the current ApplyTo method comes from the JsonPatch namespace, and we need the method with the same name but from the NewtonsoftJson namespace.<br \/>\n\u4f46\u662f\u4e00\u65e6\u6211\u4eec\u8fd9\u6837\u505a\u4e86\uff0c\u5c31\u4f1a\u5f97\u5230\u4e00\u4e2a\u9519\u8bef\u3002\u8fd9\u662f\u56e0\u4e3a\u5f53\u524d\u7684 ApplyTo \u65b9\u6cd5\u6765\u81ea JsonPatch \u547d\u540d\u7a7a\u95f4\uff0c\u6211\u4eec\u9700\u8981\u5177\u6709\u76f8\u540c\u540d\u79f0\u4f46\u6765\u81ea NewtonsoftJson \u547d\u540d\u7a7a\u95f4\u7684\u65b9\u6cd5\u3002<\/p>\n<p>Since we have the Microsoft.AspNetCore.Mvc.NewtonsoftJson package installed in the main project, we are going to remove it from there and install it in the Presentation project.<br \/>\n\u7531\u4e8e\u6211\u4eec\u5728\u4e3b\u9879\u76ee\u4e2d\u5b89\u88c5\u4e86 Microsoft.AspNetCore.Mvc.NewtonsoftJson \u5305\uff0c\u56e0\u6b64\u6211\u4eec\u5c06\u4ece\u4e3b\u9879\u76ee\u4e2d\u5220\u9664\u5b83\u5e76\u5c06\u5176\u5b89\u88c5\u5728 Presentation \u9879\u76ee\u4e2d\u3002<\/p>\n<p>If we navigate to the ApplyTo method declaration we can find two extension methods:<br \/>\n\u5982\u679c\u6211\u4eec\u5bfc\u822a\u5230 ApplyTo \u65b9\u6cd5\u58f0\u660e\uff0c\u6211\u4eec\u53ef\u4ee5\u627e\u5230\u4e24\u4e2a\u6269\u5c55\u65b9\u6cd5\uff1a<\/p>\n<pre><code>public static class JsonPatchExtensions \n{ \n    public static void ApplyTo&lt;T&gt;(this JsonPatchDocument&lt;T&gt; patchDoc, T objectToApplyTo, ModelStateDictionary modelState) where T : class... \n    public static void ApplyTo&lt;T&gt;(this JsonPatchDocument&lt;T&gt; patchDoc, T objectToApplyTo, ModelStateDictionary modelState, string prefix) where T : class... \n}<\/code><\/pre>\n<p>We are using the first one.<br \/>\n\u6211\u4eec\u6b63\u5728\u4f7f\u7528\u7b2c\u4e00\u4e2a\u3002<\/p>\n<p>After the package installation, the error in the action will disappear.<br \/>\n\u5b89\u88c5\u5305\u540e\uff0c\u4f5c\u4e2d\u7684\u9519\u8bef\u5c06\u6d88\u5931\u3002<\/p>\n<p>Now, right below thee ApplyTo method, we can add our familiar validation logic:<br \/>\n\u73b0\u5728\uff0c\u5728 ApplyTo \u65b9\u6cd5\u7684\u6b63\u4e0b\u65b9\uff0c\u6211\u4eec\u53ef\u4ee5\u6dfb\u52a0\u719f\u6089\u7684\u9a8c\u8bc1\u903b\u8f91\uff1a<\/p>\n<pre><code>patchDoc.ApplyTo(result.employeeToPatch, ModelState); \nif (!ModelState.IsValid) return UnprocessableEntity(ModelState); \n_service.EmployeeService.SaveChangesForPatch(...);<\/code><\/pre>\n<p>Let\u2019s test this now:<br \/>\n\u73b0\u5728\u8ba9\u6211\u4eec\u6d4b\u8bd5\u4e00\u4e0b\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78-2D54A9991870\/employees\/80ABBCA8-664D-4B20-B5DE-024705497D4A\">https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78-2D54A9991870\/employees\/80ABBCA8-664D-4B20-B5DE-024705497D4A<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1308.jpg\" alt=\"alt text\" \/><\/p>\n<p>You can see that it works as it is supposed to.<br \/>\n\u60a8\u53ef\u4ee5\u770b\u5230\u5b83\u6309\u9884\u671f\u5de5\u4f5c\u3002<\/p>\n<p>But, we have a small problem now. What if we try to send a remove operation, but for the valid path:<br \/>\n\u4f46\u662f\uff0c\u6211\u4eec\u73b0\u5728\u6709\u4e00\u4e2a\u5c0f\u95ee\u9898\u3002\u5982\u679c\u6211\u4eec\u5c1d\u8bd5\u53d1\u9001\u4e00\u4e2a remove\u4f5c\uff0c\u4f46\u5bf9\u4e8e\u6709\u6548\u8def\u5f84\uff0c\u8be5\u600e\u4e48\u529e\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1309.jpg\" alt=\"alt text\" \/><\/p>\n<p>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 that doesn\u2019t allow that value to be below 18. So, where is the problem?<br \/>\n\u6211\u4eec\u53ef\u4ee5\u770b\u5230\u5b83\u901a\u8fc7\uff0c\u4f46\u8fd9\u5e76\u4e0d\u597d\u3002\u5982\u679c\u60a8\u8fd8\u8bb0\u5f97\uff0c\u6211\u4eec\u8bf4\u8fc7 remove\u4f5c\u4f1a\u5c06 included \u5c5e\u6027\u7684\u503c\u8bbe\u7f6e\u4e3a\u5176\u9ed8\u8ba4\u503c\uff0c\u5373 0\u3002\u4f46\u5728 EmployeeForUpdateDto \u7c7b\u4e2d\uff0c\u6211\u4eec\u6709\u4e00\u4e2a Range \u5c5e\u6027\uff0c\u5b83\u4e0d\u5141\u8bb8\u8be5\u503c\u4f4e\u4e8e 18\u3002\u90a3\u4e48\uff0c\u95ee\u9898\u51fa\u5728\u54ea\u91cc\u5462\uff1f<\/p>\n<p>Let\u2019s illustrate this for you:<br \/>\n\u8ba9\u6211\u4eec\u4e3a\u60a8\u8bf4\u660e\u4e00\u4e0b\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1310.jpg\" alt=\"alt text\" \/><\/p>\n<p>As you can see, we are validating 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:<br \/>\n\u5982\u4f60\u6240\u89c1\uff0c\u6211\u4eec\u6b63\u5728\u9a8c\u8bc1 patchDoc\uff0c\u5b83\u76ee\u524d\u662f\u5b8c\u5168\u6709\u6548\u7684\uff0c\u4f46\u6211\u4eec\u628a employeeEntity \u4fdd\u5b58\u5230\u6570\u636e\u5e93\u4e2d\u3002\u56e0\u6b64\uff0c\u6211\u4eec\u9700\u8981\u4e00\u4e9b\u989d\u5916\u7684\u9a8c\u8bc1\u6765\u9632\u6b62\u5c06\u65e0\u6548\u7684 employeeEntity \u4fdd\u5b58\u5230\u6570\u636e\u5e93\u4e2d\uff1a<\/p>\n<pre><code>patchDoc.ApplyTo(result.employeeToPatch, ModelState); \nTryValidateModel(result.employeeToPatch); \nif (!ModelState.IsValid) return UnprocessableEntity(ModelState);<\/code><\/pre>\n<p>We can use the TryValidateModel method to validate the already patched employeeToPatch instance. This will trigger validation and every error will make ModelState invalid. After that, we execute a familiar validation check.<br \/>\n\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528 TryValidateModel \u65b9\u6cd5\u6765\u9a8c\u8bc1\u5df2\u4fee\u8865\u7684 employeeToPatch \u5b9e\u4f8b\u3002\u8fd9\u5c06\u89e6\u53d1\u9a8c\u8bc1\uff0c\u5e76\u4e14\u6bcf\u4e2a\u9519\u8bef\u90fd\u4f1a\u4f7f ModelState \u65e0\u6548\u3002\u4e4b\u540e\uff0c\u6211\u4eec\u6267\u884c\u719f\u6089\u7684\u9a8c\u8bc1\u68c0\u67e5\u3002<\/p>\n<p>Now, we can test this again:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u518d\u6b21\u6d4b\u8bd5\u4e00\u4e0b\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78-2D54A9991870\/employees\/80ABBCA8-664D\">https:\/\/localhost:5001\/api\/companies\/C9D4C053-49B6-410C-BC78-2D54A9991870\/employees\/80ABBCA8-664D<\/a>- 4B20-B5DE-024705497D4A<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/1311.jpg\" alt=\"alt text\" \/><\/p>\n<p>And we get 422, which is the expected status code.<br \/>\n\u6211\u4eec\u5f97\u5230 422\uff0c\u8fd9\u662f\u9884\u671f\u7684\u72b6\u6001\u4ee3\u7801\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>13 VALIDATION 13 \u9a8c\u8bc1 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:\u200c \u5728\u7f16\u5199 API\u52a8\u4f5c\u65f6\uff0c\u6211\u4eec\u6709\u4e00\u7ec4\u89c4\u5219\u9700\u8981\u68c0\u67e5\u3002\u5982\u679c\u6211\u4eec\u770b\u4e00\u4e0b Company \u7c7b\uff0c\u6211\u4eec\u53ef\u4ee5\u5728\u5c5e\u6027\u4e0a\u65b9\u770b\u5230\u4e0d\u540c\u7684\u6570\u636e\u6ce8\u91ca\u5c5e\u6027\uff1a Those attributes serve the purpose to validate our model object while creating or [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1124","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1124","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1124"}],"version-history":[{"count":0,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1124\/revisions"}],"wp:attachment":[{"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1124"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1124"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1124"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}