ASP.NET Core Web API 中的 JSON 修补程序

本文介绍如何处理 ASP.NET Core Web API 中的 JSON Patch 请求。

包安装

ASP.NET Core Web API 中的 JSON Patch 支持基于 Newtonsoft.Json 并且需要 Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet 包。 若要启用 JSON Patch 支持,请执行以下操作:

  • 安装 Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet 包。

  • 调用 AddNewtonsoftJson。 例如:

    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddControllers()
        .AddNewtonsoftJson();
    
    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    

AddNewtonsoftJson 替换了基于 System.Text.Json 的默认输入和输出格式化程序,该格式化程序用于设置所有JSON 内容的格式。 此扩展方法与以下 MVC 服务注册方法兼容:

JsonPatch 要求将 Content-Type 标头设置为 application/json-patch+json

使用 System.Text.Json 时添加对 JSON Patch 的支持

基于 System.Text.Json 的输入格式化程序不支持 JSON Patch。 若要使用 Newtonsoft.Json 添加对 JSON Patch 的支持,同时使其他输入和输出格式化程序保持不变:

  • 安装 Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet 包。

  • 更新Program.cs

    using JsonPatchSample;
    using Microsoft.AspNetCore.Mvc.Formatters;
    
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddControllers(options =>
    {
        options.InputFormatters.Insert(0, MyJPIF.GetJsonPatchInputFormatter());
    });
    
    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Formatters;
    using Microsoft.Extensions.Options;
    
    namespace JsonPatchSample;
    
    public static class MyJPIF
    {
        public static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter()
        {
            var builder = new ServiceCollection()
                .AddLogging()
                .AddMvc()
                .AddNewtonsoftJson()
                .Services.BuildServiceProvider();
    
            return builder
                .GetRequiredService<IOptions<MvcOptions>>()
                .Value
                .InputFormatters
                .OfType<NewtonsoftJsonPatchInputFormatter>()
                .First();
        }
    }
    

前面的代码创建 NewtonsoftJsonPatchInputFormatter 的实例,并将其作为 MvcOptions.InputFormatters 集合中的第一个条目插入。 此注册顺序可确保:

  • NewtonsoftJsonPatchInputFormatter 处理 JSON Patch 请求。
  • 基于 System.Text.Json 的现有输入和格式化程序处理所有其他 JSON 请求和响应。

使用 Newtonsoft.Json.JsonConvert.SerializeObject 方法序列化 JsonPatchDocument

PATCH HTTP 请求方法

PUT 和 PATCH 方法用于更新现有资源。 它们之间的区别是,PUT 会替换整个资源,而PATCH 仅指定更改。

JSON Patch

JSON Patch 是一种格式,用于指定要应用于资源的更新。 JSON Patch 文档有一个操作数组。 每个操作都标识一种特定类型的更改。 此类更改的示例包括添加数组元素或替换属性值。

例如,以下 JSON 文档表示资源、资源的 JSON Patch 文档和应用 Patch 操作的结果。

资源示例

{
  "customerName": "John",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    }
  ]
}

JSON Patch 示例

[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

在上述 JSON 中:

  • op 属性指示操作的类型。
  • path 属性指示要更新的元素。
  • value 属性提供新值。

修补程序之后的资源

下面是应用上述 JSON Patch 文档后的资源:

{
  "customerName": "Barry",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    },
    {
      "orderName": "Order2",
      "orderType": null
    }
  ]
}

通过将 JSON Patch 文档应用于资源所做的更改是原子操作。 如果列表中的任何操作失败,则不会应用列表中的任何操作。

路径语法

操作对象的路径属性的级别之间有斜杠。 例如,"/address/zipCode"

使用从零开始的索引来指定数组元素。 addresses 数组的第一个元素将位于 /addresses/0。 若要将 add 置于数组末尾,请使用连字符 (-),而不是索引号:/addresses/-

Operations

下表显示了 JSON Patch 规范中定义的支持操作:

操作 说明
add 添加属性或数组元素。 对于现有属性:设置值。
remove 删除属性或数组元素。
replace 与在相同位置后跟 addremove 相同。
move 与从后跟 add 的源到使用源中的值的目标的 remove 相同。
copy 与到使用源中的值的目标的 add 相同。
test 如果 path 处的值 = 提供的 value,则返回成功状态代码。

ASP.NET Core 中的 JSON Patch

Microsoft.AspNetCore.JsonPatch NuGet 包中提供了 JSON Patch 的 ASP.NET Core 实现。

操作方法代码

在 API 控制器中,JSON Patch 的操作方法:

下面是一个示例:

[HttpPatch]
public IActionResult JsonPatchWithModelState(
    [FromBody] JsonPatchDocument<Customer> patchDoc)
{
    if (patchDoc != null)
    {
        var customer = CreateCustomer();

        patchDoc.ApplyTo(customer, ModelState);

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

        return new ObjectResult(customer);
    }
    else
    {
        return BadRequest(ModelState);
    }
}

示例应用中的此代码适用于以下 Customer 模型:

namespace JsonPatchSample.Models;

public class Customer
{
    public string? CustomerName { get; set; }
    public List<Order>? Orders { get; set; }
}
namespace JsonPatchSample.Models;

public class Order
{
    public string OrderName { get; set; }
    public string OrderType { get; set; }
}

示例操作方法:

  • 构造一个 Customer
  • 应用修补程序。
  • 在响应的正文中返回结果。

在实际应用中,该代码将从存储(如数据库)中检索数据并在应用修补程序后更新数据库。

模型状态

上述操作方法示例调用将模型状态用作其参数之一的 ApplyTo 的重载。 使用此选项,可以在响应中收到错误消息。 以下示例显示了 test 操作的“400 错误请求”响应的正文:

{
  "Customer": [
    "The current value 'John' at path 'customerName' != test value 'Nancy'."
  ]
}

动态对象

以下操作方法示例演示如何将修补程序应用于动态对象:

[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
    dynamic obj = new ExpandoObject();
    patch.ApplyTo(obj);

    return Ok(obj);
}

添加操作

  • 如果 path 指向数组元素:将新元素插入到 path 指定的元素之前。
  • 如果 path 指向属性:设置属性值。
  • 如果 path 指向不存在的位置:
    • 如果要修补的资源是一个动态对象:添加属性。
    • 如果要修补的资源是一个静态对象:请求失败。

以下示例修补程序文档设置 CustomerName 的值,并将 Order 对象添加到 Orders 数组末尾。

[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

删除操作

  • 如果 path 指向数组元素:删除元素。
  • 如果 path 指向属性:
    • 如果要修补的资源是一个动态对象:删除属性。
    • 如果要修补的资源是一个静态对象:
      • 如果属性可以为 Null:将其设置为 null。
      • 如果属性不可以为 Null,将其设置为 default<T>

以下示例修补程序文档将 CustomerName 设置为 null 并删除 Orders[0]

[
  {
    "op": "remove",
    "path": "/customerName"
  },
  {
    "op": "remove",
    "path": "/orders/0"
  }
]

替换操作

此操作在功能上与后跟 addremove 相同。

以下示例修补程序文档设置 CustomerName 的值,并将 Orders[0] 替换为新的 Order 对象:

[
  {
    "op": "replace",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "replace",
    "path": "/orders/0",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

移动操作

  • 如果 path 指向数组元素:将 from 元素复制到 path 元素的位置,然后对 from 元素运行 remove 操作。
  • 如果 path 指向属性:将 from 属性的值复制到 path 属性,然后对 from 属性运行 remove 操作。
  • 如果 path 指向不存在的属性:
    • 如果要修补的资源是一个静态对象:请求失败。
    • 如果要修补的资源是一个动态对象:将 from 属性复制到 path 指示的位置,然后对 from 属性运行 remove 操作。

以下示例修补程序文档:

  • Orders[0].OrderName 的值复制到 CustomerName
  • Orders[0].OrderName 设置为 null。
  • Orders[1] 移动到 Orders[0] 之前。
[
  {
    "op": "move",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "move",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

复制操作

此操作在功能上与不包含最后 remove 步骤的 move 操作相同。

以下示例修补程序文档:

  • Orders[0].OrderName 的值复制到 CustomerName
  • Orders[1] 的副本插入到 Orders[0] 之前。
[
  {
    "op": "copy",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "copy",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

测试操作

如果 path 指示的位置处的值与 value 中提供的值不同,则请求会失败。 在这种情况下,整个 PATCH 请求会失败,即使修补程序文档中的所有其他操作均成功也是如此。

test 操作通常用于在发生并发冲突时阻止更新。

如果 CustomerName 的初始值是“John”,则以下示例修补程序文档不起任何作用,因为测试失败:

[
  {
    "op": "test",
    "path": "/customerName",
    "value": "Nancy"
  },
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  }
]

获取代码

查看或下载示例代码。 (下载方法)。

若要测试此示例,请使用以下设置运行应用并发送 HTTP 请求:

  • URL: http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
  • HTTP 方法:PATCH
  • 标头:Content-Type: application/json-patch+json
  • 正文:从 JSON 项目文件夹中复制并粘贴其中一个 JSON Patch 文档示例。

其他资源

本文介绍如何处理 ASP.NET Core Web API 中的 JSON Patch 请求。

包安装

若要在应用中启用 JSON Patch 支持,请完成以下步骤:

  1. 安装 Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet 包。

  2. 更新项目的 Startup.ConfigureServices 方法以调用 AddNewtonsoftJson。 例如:

    services
        .AddControllersWithViews()
        .AddNewtonsoftJson();
    

AddNewtonsoftJson 与 MVC 服务注册方法兼容:

JSON Patch、AddNewtonsoftJson 和 System.Text.Json

AddNewtonsoftJson 替换了基于 System.Text.Json 的输入和输出格式化程序,该格式化程序用于设置所有JSON 内容的格式。 若要使用 Newtonsoft.Json 添加对 JSON Patch 的支持,同时使其他格式化程序保持不变,请按如下所示更新项目的 Startup.ConfigureServices 方法:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        options.InputFormatters.Insert(0, GetJsonPatchInputFormatter());
    });
}

private static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter()
{
    var builder = new ServiceCollection()
        .AddLogging()
        .AddMvc()
        .AddNewtonsoftJson()
        .Services.BuildServiceProvider();

    return builder
        .GetRequiredService<IOptions<MvcOptions>>()
        .Value
        .InputFormatters
        .OfType<NewtonsoftJsonPatchInputFormatter>()
        .First();
}

上述代码需要 Microsoft.AspNetCore.Mvc.NewtonsoftJson 包和以下 using 语句:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using System.Linq;

使用 Newtonsoft.Json.JsonConvert.SerializeObject 方法可序列化 JsonPatchDocument。

PATCH HTTP 请求方法

PUT 和 PATCH 方法用于更新现有资源。 它们之间的区别是,PUT 会替换整个资源,而PATCH 仅指定更改。

JSON Patch

JSON Patch 是一种格式,用于指定要应用于资源的更新。 JSON Patch 文档有一个操作数组。 每个操作都标识一种特定类型的更改。 此类更改的示例包括添加数组元素或替换属性值。

例如,以下 JSON 文档表示资源、资源的 JSON Patch 文档和应用 Patch 操作的结果。

资源示例

{
  "customerName": "John",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    }
  ]
}

JSON Patch 示例

[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

在上述 JSON 中:

  • op 属性指示操作的类型。
  • path 属性指示要更新的元素。
  • value 属性提供新值。

修补程序之后的资源

下面是应用上述 JSON Patch 文档后的资源:

{
  "customerName": "Barry",
  "orders": [
    {
      "orderName": "Order0",
      "orderType": null
    },
    {
      "orderName": "Order1",
      "orderType": null
    },
    {
      "orderName": "Order2",
      "orderType": null
    }
  ]
}

通过将 JSON Patch 文档应用于资源所做的更改是原子操作。 如果列表中的任何操作失败,则不会应用列表中的任何操作。

路径语法

操作对象的路径属性的级别之间有斜杠。 例如,"/address/zipCode"

使用从零开始的索引来指定数组元素。 addresses 数组的第一个元素将位于 /addresses/0。 若要将 add 置于数组末尾,请使用连字符 (-),而不是索引号:/addresses/-

Operations

下表显示了 JSON Patch 规范中定义的支持操作:

操作 说明
add 添加属性或数组元素。 对于现有属性:设置值。
remove 删除属性或数组元素。
replace 与在相同位置后跟 addremove 相同。
move 与从后跟 add 的源到使用源中的值的目标的 remove 相同。
copy 与到使用源中的值的目标的 add 相同。
test 如果 path 处的值 = 提供的 value,则返回成功状态代码。

ASP.NET Core 中的 JSON Patch

Microsoft.AspNetCore.JsonPatch NuGet 包中提供了 JSON Patch 的 ASP.NET Core 实现。

操作方法代码

在 API 控制器中,JSON Patch 的操作方法:

  • 使用 HttpPatch 属性进行批注。
  • 接受 JsonPatchDocument<T>,通常带有 [FromBody]
  • 在修补程序文档上调用 ApplyTo 以应用更改。

下面是一个示例:

[HttpPatch]
public IActionResult JsonPatchWithModelState(
    [FromBody] JsonPatchDocument<Customer> patchDoc)
{
    if (patchDoc != null)
    {
        var customer = CreateCustomer();

        patchDoc.ApplyTo(customer, ModelState);

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

        return new ObjectResult(customer);
    }
    else
    {
        return BadRequest(ModelState);
    }
}

示例应用中的此代码适用于以下 Customer 模型:

using System.Collections.Generic;

namespace JsonPatchSample.Models
{
    public class Customer
    {
        public string CustomerName { get; set; }
        public List<Order> Orders { get; set; }
    }
}
namespace JsonPatchSample.Models
{
    public class Order
    {
        public string OrderName { get; set; }
        public string OrderType { get; set; }
    }
}

示例操作方法:

  • 构造一个 Customer
  • 应用修补程序。
  • 在响应的正文中返回结果。

在实际应用中,该代码将从存储(如数据库)中检索数据并在应用修补程序后更新数据库。

模型状态

上述操作方法示例调用将模型状态用作其参数之一的 ApplyTo 的重载。 使用此选项,可以在响应中收到错误消息。 以下示例显示了 test 操作的“400 错误请求”响应的正文:

{
    "Customer": [
        "The current value 'John' at path 'customerName' is not equal to the test value 'Nancy'."
    ]
}

动态对象

以下操作方法示例演示如何将修补程序应用于动态对象:

[HttpPatch]
public IActionResult JsonPatchForDynamic([FromBody]JsonPatchDocument patch)
{
    dynamic obj = new ExpandoObject();
    patch.ApplyTo(obj);

    return Ok(obj);
}

添加操作

  • 如果 path 指向数组元素:将新元素插入到 path 指定的元素之前。
  • 如果 path 指向属性:设置属性值。
  • 如果 path 指向不存在的位置:
    • 如果要修补的资源是一个动态对象:添加属性。
    • 如果要修补的资源是一个静态对象:请求失败。

以下示例修补程序文档设置 CustomerName 的值,并将 Order 对象添加到 Orders 数组末尾。

[
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "add",
    "path": "/orders/-",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

删除操作

  • 如果 path 指向数组元素:删除元素。
  • 如果 path 指向属性:
    • 如果要修补的资源是一个动态对象:删除属性。
    • 如果要修补的资源是一个静态对象:
      • 如果属性可以为 Null:将其设置为 null。
      • 如果属性不可以为 Null,将其设置为 default<T>

以下示例修补程序文档将 CustomerName 设置为 null 并删除 Orders[0]

[
  {
    "op": "remove",
    "path": "/customerName"
  },
  {
    "op": "remove",
    "path": "/orders/0"
  }
]

替换操作

此操作在功能上与后跟 addremove 相同。

以下示例修补程序文档设置 CustomerName 的值,并将 Orders[0] 替换为新的 Order 对象:

[
  {
    "op": "replace",
    "path": "/customerName",
    "value": "Barry"
  },
  {
    "op": "replace",
    "path": "/orders/0",
    "value": {
      "orderName": "Order2",
      "orderType": null
    }
  }
]

移动操作

  • 如果 path 指向数组元素:将 from 元素复制到 path 元素的位置,然后对 from 元素运行 remove 操作。
  • 如果 path 指向属性:将 from 属性的值复制到 path 属性,然后对 from 属性运行 remove 操作。
  • 如果 path 指向不存在的属性:
    • 如果要修补的资源是一个静态对象:请求失败。
    • 如果要修补的资源是一个动态对象:将 from 属性复制到 path 指示的位置,然后对 from 属性运行 remove 操作。

以下示例修补程序文档:

  • Orders[0].OrderName 的值复制到 CustomerName
  • Orders[0].OrderName 设置为 null。
  • Orders[1] 移动到 Orders[0] 之前。
[
  {
    "op": "move",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "move",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

复制操作

此操作在功能上与不包含最后 remove 步骤的 move 操作相同。

以下示例修补程序文档:

  • Orders[0].OrderName 的值复制到 CustomerName
  • Orders[1] 的副本插入到 Orders[0] 之前。
[
  {
    "op": "copy",
    "from": "/orders/0/orderName",
    "path": "/customerName"
  },
  {
    "op": "copy",
    "from": "/orders/1",
    "path": "/orders/0"
  }
]

测试操作

如果 path 指示的位置处的值与 value 中提供的值不同,则请求会失败。 在这种情况下,整个 PATCH 请求会失败,即使修补程序文档中的所有其他操作均成功也是如此。

test 操作通常用于在发生并发冲突时阻止更新。

如果 CustomerName 的初始值是“John”,则以下示例修补程序文档不起任何作用,因为测试失败:

[
  {
    "op": "test",
    "path": "/customerName",
    "value": "Nancy"
  },
  {
    "op": "add",
    "path": "/customerName",
    "value": "Barry"
  }
]

获取代码

查看或下载示例代码。 (下载方法)。

若要测试此示例,请使用以下设置运行应用并发送 HTTP 请求:

  • URL: http://localhost:{port}/jsonpatch/jsonpatchwithmodelstate
  • HTTP 方法:PATCH
  • 标头:Content-Type: application/json-patch+json
  • 正文:从 JSON 项目文件夹中复制并粘贴其中一个 JSON Patch 文档示例。

其他资源