Ultimate ASP.NET Core Web API 12 WORKING WITH PATCH REQUESTS

12 WORKING WITH PATCH REQUESTS

12 使用PATCH请求

In the previous chapter, we worked with the PUT request to fully update our resource. But if we want to update our resource only partially, we should use PATCH.‌
在上一章中,我们使用了 PUT 请求来完全更新我们的资源。但是,如果我们想只部分更新我们的资源,我们应该使用 PATCH。

The partial update isn’t the only difference between PATCH and PUT. The request body is different as well. For the Company PATCH request, for example, we should use [FromBody]JsonPatchDocument<Company> and not [FromBody]Company as we did with the PUT requests.
部分更新并不是 PATCH 和 PUT 之间的唯一区别。请求正文也不同。例如,对于 Company PATCH 请求,我们应该使用 [FromBody]JsonPatchDocument<Company>,而不是 [FromBody]Company,就像我们对 PUT 请求所做的那样。

Additionally, for the PUT request’s media type, we have used application/json — but for the PATCH request’s media type, we should use application/json-patch+json. Even though the first one would be accepted in ASP.NET Core for the PATCH request, the recommendation by REST standards is to use the second one.
此外,对于 PUT 请求的媒体类型,我们使用了 application/json,但对于 PATCH 请求的媒体类型,我们应该使用 application/json-patch+json。尽管 PATCH 请求的 ASP.NET Core 会接受第一个选项,但 REST 标准建议使用第二个选项。

Let’s see what the PATCH request body looks like:
让我们看看 PATCH 请求正文是什么样子的:

[ { "op": "replace", "path": "/name", "value": "new name" }, { "op": "remove", "path": "/name" } ]

The square brackets represent an array of operations. Every operation is placed between curly brackets. So, in this specific example, we have two operations: Replace and Remove represented by the op property. The path property represents the object’s property that we want to modify and the value property represents a new value.
方括号表示一组作。每个作都放在大括号之间。因此,在这个特定示例中,我们有两个作:由 op 属性表示的 Replace 和 Remove。path 属性表示我们要修改的对象属性,value 属性表示新值。

In this specific example, for the first operation, we replace the value of the name property with a new name. In the second example, we remove the name property, thus setting its value to default.
在这个特定示例中,对于第一个作,我们将 name 属性的值替换为新名称。在第二个示例中,我们删除了 name 属性,从而将其值设置为 default。

There are six different operations for a PATCH request:
PATCH 请求有 6 种不同的操作作:

OPERATION REQUEST BODY EXPLANATION
Add { "op": "add", "path": "/name", "value": "new value" } Assigns a new value to a required property.
Remove { "op": "remove","path": "/name"} Sets a default value to a required property.
Replace { "op": "replace", "path": "/name", "value": "new value" } Replaces a value of a required property to a new value.
Copy {"op": "copy","from": "/name","path": "/title"} Copies the value from a property in the “from” part to the property in the “path” part.
Move { "op": "move", "from": "/name", "path": "/title" } Moves the value from a property in the “from” part to a property in the “path” part.
Test {"op": "test","path": "/name","value": "new value"} Tests if a property has a specified value.

After all this theory, we are ready to dive into the coding part.
在所有这些理论之后,我们准备深入研究编码部分。

12.1 Applying PATCH to the Employee Entity

12.1 将 PATCH 应用于 Employee 实体

Before we start with the code modification, we have to install two required libraries:‌
在我们开始修改代码之前,我们必须安装两个必需的库:

• The Microsoft.AspNetCore.JsonPatch library, in the Presentation project, to support the usage of JsonPatchDocument in our controller and
Presentation项目中的 Microsoft.AspNetCore.JsonPatch 库,用于支持在控制器中使用 JsonPatchDocument 和

• The Microsoft.AspNetCore.Mvc.NewtonsoftJson library, in the main project, to support request body conversion to a PatchDocument once we send our request.
主项目中的 Microsoft.AspNetCore.Mvc.NewtonsoftJson 库,用于支持在发送请求后将请求正文转换为 PatchDocument。

As you can see, we are still using the NewtonsoftJson library to support the PatchDocument conversion. The official statement from Microsoft is that they are not going to replace it with System.Text.Json: “The main reason is that this will require a huge investment from us, with not a very high value-add for the majority of our customers.”.
如您所见,我们仍在使用 NewtonsoftJson 库来支持 PatchDocument 转换。Microsoft 的官方声明是,他们不会用 System.Text.Json 替换它:“主要原因是这需要我们进行大量投资,对于我们的大多数客户来说,附加值不是很高。

By using AddNewtonsoftJson, we are replacing the System.Text.Json formatters for all JSON content. We don’t want to do that so, we are going ton add a simple workaround in the Program class:
通过使用 AddNewtonsoftJson,我们将替换所有 JSON 内容的 System.Text.Json 格式化程序。我们不想这样做,因此,我们将在 Program 类中添加一个简单的解决方法:

NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter() => 
    new ServiceCollection()
    .AddLogging()
    .AddMvc()
    .AddNewtonsoftJson()
    .Services.BuildServiceProvider()
    .GetRequiredService<IOptions<MvcOptions>>()
    .Value.InputFormatters.OfType<NewtonsoftJsonPatchInputFormatter>()
    .First();

By adding a method like this in the Program class, we are creating a local function. This function configures support for JSON Patch using Newtonsoft.Json while leaving the other formatters unchanged.
通过在 Program 类中添加这样的方法,我们正在创建一个本地函数。此函数使用 Newtonsoft.Json 配置对 JSON Patch 的支持,同时保持其他格式化程序不变。

For this to work, we have to include two more namespaces in the class:
为此,我们必须在 class 中再包含两个命名空间:

using Microsoft.AspNetCore.Mvc.Formatters; 
using Microsoft.Extensions.Options;

After that, we have to modify the AddControllers method:
之后,我们必须修改 AddControllers 方法:

builder.Services.AddControllers(config => { 
    config.RespectBrowserAcceptHeader = true; 
    config.ReturnHttpNotAcceptable = true; 
    config.InputFormatters.Insert(0, GetJsonPatchInputFormatter()); 
}).AddXmlDataContractSerializerFormatters()
.AddCustomCSVFormatter()
.AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference)
.Assembly);

// config.InputFormatters.Insert(0, GetJsonPatchInputFormatter());

We are placing our JsonPatchInputFormatter at the index 0 in the InputFormatters list.
我们将 JsonPatchInputFormatter 放在 InputFormatters 列表中的索引 0 处。

We will require a mapping from the Employee type to the EmployeeForUpdateDto type. Therefore, we have to create a mapping rule for that.
我们需要从 Employee 类型到 EmployeeForUpdateDto 类型的映射。因此,我们必须为此创建一个映射规则。

If we take a look at the MappingProfile class, we will see that we have a mapping from the EmployeeForUpdateDto to the Employee type:
如果我们看一下 MappingProfile 类,我们将看到我们有一个从 EmployeeForUpdateDto 到 Employee 类型的映射:

CreateMap<EmployeeForUpdateDto, Employee>();

But we need it another way. To do so, we are not going to create an additional rule; we can just use the ReverseMap method to help us in the process:
但我们需要另一种方式。为此,我们不会创建其他规则;我们可以使用 ReverseMap 方法来在此过程中帮助我们:

CreateMap<EmployeeForUpdateDto, Employee>().ReverseMap();

The ReverseMap method is also going to configure this rule to execute reverse mapping if we ask for it.
ReverseMap 方法还将配置此规则,以便在我们要求时执行反向映射。

After that, we are going to add two new method contracts to the IEmployeeService interface:
之后,我们将向 IEmployeeService 接口添加两个新的方法协定:

using Entities.Models;
using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface IEmployeeService
    {
        IEnumerable<EmployeeDto> GetEmployees(Guid companyId, bool trackChanges);
        EmployeeDto GetEmployee(Guid companyId, Guid id, bool trackChanges);
        EmployeeDto CreateEmployeeForCompany(Guid companyId, EmployeeForCreationDto employeeForCreation, bool trackChanges);
        void DeleteEmployeeForCompany(Guid companyId, Guid id, bool trackChanges);
        void UpdateEmployeeForCompany(Guid companyId, Guid id, EmployeeForUpdateDto employeeForUpdate, bool compTrackChanges, bool empTrackChanges);
        (EmployeeForUpdateDto employeeToPatch, Employee employeeEntity) GetEmployeeForPatch(Guid companyId, Guid id, bool compTrackChanges, bool empTrackChanges); 
        void SaveChangesForPatch(EmployeeForUpdateDto employeeToPatch, Employee employeeEntity);
    }
}

Of course, for this to work, we have to add the reference to the Entities project.
当然,要使其正常工作,我们必须添加对 Entities 项目的引用。

Then, we have to implement these two methods in the EmployeeService class:
然后,我们必须在 EmployeeService 类中实现这两个方法:

public (EmployeeForUpdateDto employeeToPatch, Employee employeeEntity) GetEmployeeForPatch(Guid companyId, Guid id, bool compTrackChanges, bool empTrackChanges)
{
    var company = _repository.Company.GetCompany(companyId, compTrackChanges);
    if (company is null)
        throw new CompanyNotFoundException(companyId);
    var employeeEntity = _repository.Employee.GetEmployee(companyId, id, empTrackChanges);
    if (employeeEntity is null)
        throw new EmployeeNotFoundException(companyId);
    var employeeToPatch = _mapper.Map<EmployeeForUpdateDto>(employeeEntity);
    return (employeeToPatch, employeeEntity);
}
public void SaveChangesForPatch(EmployeeForUpdateDto employeeToPatch, Employee employeeEntity)
{
    _mapper.Map(employeeToPatch, employeeEntity);
    _repository.Save();
}

In the first method, we are trying to fetch both the company and employee from the database and if we can’t find either of them, we stop the execution flow and return the NotFound response to the client. Then, we map the employee entity to the EmployeeForUpdateDto type and return both objects (employeeToPatch and employeeEntity) inside the Tuple to the controller.
在第一种方法中,我们尝试从数据库中获取公司和员工,如果找不到他们中的任何一个,我们将停止执行流并将 NotFound 响应返回给客户端。然后,我们将 employee 实体映射到 EmployeeForUpdateDto 类型,并将 Tuple 中的两个对象(employeeToPatch 和 employeeEntity)返回给控制器。

The second method just maps from emplyeeToPatch to employeeEntity and calls the repository's Save method.
第二个方法只是从 emplyeeToPatch 映射到 employeeEntity,并调用存储库的 Save 方法。

Now, we can modify our controller:
现在,我们可以修改我们的控制器:

[HttpPatch("{id:guid}")]
public IActionResult PartiallyUpdateEmployeeForCompany(Guid companyId, Guid id, [FromBody] JsonPatchDocument<EmployeeForUpdateDto> patchDoc)
{
    if (patchDoc is null)
        return BadRequest("patchDoc object sent from client is null.");
    var result = _service.EmployeeService.GetEmployeeForPatch(companyId, id, compTrackChanges: false, empTrackChanges: true);
    patchDoc.ApplyTo(result.employeeToPatch);
    _service.EmployeeService.SaveChangesForPatch(result.employeeToPatch, result.employeeEntity);
    return NoContent();
}

You can see that our action signature is different from the PUT actions. We are accepting the JsonPatchDocument from the request body. After that, we have a familiar code where we check the patchDoc for null value and if it is, we return a BadRequest. Then we call the service method where we map from the Employee type to the EmployeeForUpdateDto type; we need to do that because the patchDoc variable can apply only to the EmployeeForUpdateDto type. After apply is executed, we call another service method to map again to the Employee type (from employeeToPatch to employeeEntity) and save changes in the database. In the end, we return NoContent.
您可以看到我们的作签名与 PUT作不同。我们正在接受来自请求正文的 JsonPatchDocument。之后,我们有一个熟悉的代码,我们在其中检查 patchDoc 是否为 null 值,如果为空,则返回一个 BadRequest。然后,我们调用服务方法,从其中 Employee 类型映射到 EmployeeForUpdateDto 类型;我们需要这样做,因为 patchDoc 变量只能应用于 EmployeeForUpdateDto 类型。执行 apply 后,我们调用另一个服务方法再次映射到 Employee 类型(从 employeeToPatch 到 employeeEntity)并将更改保存在数据库中。最后,我们返回 NoContent。

Don’t forget to include an additional namespace:
不要忘记包含额外的命名空间:

using Microsoft.AspNetCore.JsonPatch;

Now, we can send a couple of requests to test this code:
现在,我们可以发送几个请求来测试此代码:

Let’s first send the replace operation:
让我们首先发送 replace作:

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

alt text

It works; we get the 204 No Content message. Let’s check the same employee:
它有效;我们收到 204 No Content 消息。让我们检查同一名员工:

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

alt text

And we see the Age property has been changed.
我们看到 Age 属性已更改。

Let’s send a remove operation in a request:
让我们在请求中发送一个 remove作:

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

alt text

This works as well. Now, if we check our employee, its age is going to be set to 0 (the default value for the int type):
这也有效。现在,如果我们检查我们的员工,它的 age 将被设置为 0(int 类型的默认值):

alt text

Finally, let’s return a value of 28 for the Age property:
最后,让我们为 Age 属性返回值 28:

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

alt text

Let’s check the employee now:
现在让我们检查一下员工:

alt text

Excellent.
非常好。

Everything works as expected.
一切都按预期进行。

Leave a Reply

Your email address will not be published. Required fields are marked *