ASP.NET Core Web API 中的 JsonPatch

本文說明如何處理 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 會取代用於格式化所有JSON 內容的預設 System.Text.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 屬性會提供新值。

修補之後的資源

以下是套用上述 Patch JSON Patch 文件之後的資源:

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

將 JSON Patch 文件套用至資源所做的變更是不可部分完成的。 如果清單中有任何作業失敗,則不會套用清單中的任何作業。

路徑語法

作業物件的 path \(英文\) 屬性在層級之間有斜線。 例如: "/address/zipCode"

以零為起始的索引可用來指定陣列元素。 addresses 陣列的第一個元素會在 /addresses/0 上。 若要將 add 到陣列結尾處,請使用連字號 (-) 而不是索引號碼:/addresses/-

Operations

下表顯示支援的作業,如 JSON Patch 規格中所定義:

作業 備註
add 加入屬性或陣列元素。 針對現有的屬性:設定值。
remove 移除屬性或陣列元素。
replace remove 之後接著在同一個位置上 add 相同。
move 與從來源 remove 之後接著使用來源的值 add 到目的地相同。
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"
  }
]

取代作業

此作業在功能上與 remove 之後接著 add 相同。

下列範例修補文件會設定 CustomerName 的值,並使用新的 Order 物件來取代 Orders[0]

[
  {
    "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[0] 前面插入 Orders[1] 的複本。
[
  {
    "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 會取代用於格式化所有JSON 內容的預設 System.Text.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 屬性會提供新值。

修補之後的資源

以下是套用上述 Patch JSON Patch 文件之後的資源:

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

將 JSON Patch 文件套用至資源所做的變更是不可部分完成的。 如果清單中有任何作業失敗,則不會套用清單中的任何作業。

路徑語法

作業物件的 path \(英文\) 屬性在層級之間有斜線。 例如: "/address/zipCode"

以零為起始的索引可用來指定陣列元素。 addresses 陣列的第一個元素會在 /addresses/0 上。 若要將 add 到陣列結尾處,請使用連字號 (-) 而不是索引號碼:/addresses/-

Operations

下表顯示支援的作業,如 JSON Patch 規格中所定義:

作業 備註
add 加入屬性或陣列元素。 針對現有的屬性:設定值。
remove 移除屬性或陣列元素。
replace remove 之後接著在同一個位置上 add 相同。
move 與從來源 remove 之後接著使用來源的值 add 到目的地相同。
copy 與使用來源的值 add 到目的地相同。
test 如果 path 上的值 = 所提供的 value,即會傳回成功狀態碼。

ASP.NET Core 中的 JSON Patch

Microsoft.AspNetCore.JsonPatch NuGet 套件中會提供 JSON Patch 的 ASP.NET Core 實作。

動作方法程式碼

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

  • 使用 HttpPatch 屬性來標註。
  • 通常會使用 [FromBody] 來接受 JsonPatchDocument<T>
  • 呼叫修補文件上的 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"
  }
]

取代作業

此作業在功能上與 remove 之後接著 add 相同。

下列範例修補文件會設定 CustomerName 的值,並使用新的 Order 物件來取代 Orders[0]

[
  {
    "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[0] 前面插入 Orders[1] 的複本。
[
  {
    "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 文件範例。

其他資源