Note: This article was originally published on 04/05/2021.
These two articles are to discuss JSON Patch.
A: Introduction
In the ASP.NET fields, both MVC and Web API modules, we are fimilar with creating the controller based on the output entity object to get CRUD actions for us, i.e., the Verbs: GET, POST, PUT, and DELETE:
- GET --- Select
- POST --- Create
- PUT --- Update
- DELETE --- Delete
These methods are used to access database.
Actually, there is one more Verb: Patch, available. Then we have the Verbs like this
The Methods and Verbs
The patch method is responsible to the patial update. We will discuss this issue in this article. We will discuss in the order below:
- A - Introduction
- B - What is Patch
- Full Update vs. Patial Update
- What is JSON Patch?
- How Json Patch works?
- C - Patch Implementation in ASP.NET Core Web API, in Memory
- Configure the App
- Add PATCH related files (Entities and Controller)
- Discussion of Operations
- Add Operation
- Remove Operation
- Replace Operation
- Move Operation
- Copy Operation
- Test Operation
- Implementation in ASP.NET Core Web API, for Database
B: What is Patch
1, Full Update vs. Patial Update
The Verb PUT update the database the whole record. However, sometimes, the client only want to update one field or partial fields in the records, sometimes, client side even does not have full info to update the records in whole, in this situation, a partial update is a better solution or required, that is what PATCH jumps in.
Therefore, the HTTP Verbs will be like this,
2, What is JSON Patch?
JSON Patch is a format for describing changes to a JSON document. It can be used to avoid sending a whole document when only a part has changed. When used in combination with the HTTP PATCH method, it allows partial updates for HTTP APIs in a standards compliant way.
JSON Patch is specified in RFC 6902 from the IETF.
3, How Json Patch works?
A JSON Patch document is just a JSON file containing an array of patch operations. The patch operations supported by JSON Patch are “add”, “remove”, “replace”, “move”, “copy” and “test”. The operations are applied in order: if any of them fail then the whole patch operation should abort.
C: Patch Implementation in ASP.NET Core Web API, in Memory
We will demo the PATCH result in ASP.NET Core Web API app with Swagger support.
1, Configure the App
Step 1: Create an ASP.NET Core MVC application
We use the current version of Visual Studio 2019 16.9.3 and .NET 5.0 SDK to Create an ASP.NET Core Web API app, see here for details.
Step 2: Configure app
To enable JSON Patch support, we need to install the Microsoft.AspNetCore.Mvc.NewtonsoftJson from NuGet package
2, Add PATCH related files:
Step 1: Add two entity classes:
Add two classes: Customer and Order:
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; }
}
}
Step 2: Add a Controller:
Add a tmpty controller named as: HomeController with following code:
using JsonPatchSample.Models;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Dynamic;
namespace JsonPatchSample.Controllers
{
[Route("jsonpatch/[action]")]
[ApiController]
public class HomeController : ControllerBase
{
[HttpGet]
public IActionResult JsonPatchWithModelState()
{
var customer = CreateCustomer();
return new ObjectResult(customer);
}
[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);
}
}
private Customer CreateCustomer()
{
return new Customer
{
CustomerName = "John",
Orders = new List<Order>()
{
new Order
{
OrderName = "Order0"
},
new Order
{
OrderName = "Order1"
}
}
};
}
}
}
3, Discussion of Operations:
Run the App:
For the .NET Core 5.0, there is a very good Swagger support for PATCH. If you run a previous version of .NET Core, it might not be the same.
Run the App, we have two action methods: GET and PATCH. Run the GET, we will get the Original input data:
Run PATCH, we will run the PATCH operations: “add”, “remove”, “replace”, “move”, “copy” and “test”, one by one, all results are against to the original one above:
1. The add operation
- If
path
points to an array element: inserts new element before the one specified by path
.
- If
path
points to a property: sets the property value.
- If
path
points to a nonexistent location:
- If the resource to patch is a dynamic object: adds a property.
- If the resource to patch is a static object: the request fails.
The following sample patch document sets the value of CustomerName
and adds an Order
object to the end of the Orders
array.
[
{
"op": "add",
"path": "/customerName",
"value": "Barry"
},
{
"op": "add",
"path": "/orders/-",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
Shown in app like this,
In the preceding JSON:
- The
op
property indicates the type of operation.
- The
path
property indicates the element to update.
- The
value
property provides the new value.
Then we got the result:
The changes made by applying a JSON Patch document to a resource are atomic. If any operation in the list fails, no operation in the list is applied.
2. The remove operation
- If
path
points to an array element: removes the element.
- If
path
points to a property:
- If resource to patch is a dynamic object: removes the property.
- If resource to patch is a static object:
- If the property is nullable: sets it to null.
- If the property is non-nullable, sets it to
default<T>
.
The following sample patch document sets CustomerName
to null and deletes Orders[0]
:
JSON
[
{
"op": "remove",
"path": "/customerName"
},
{
"op": "remove",
"path": "/orders/0"
}
]
Then we got the result:
3. The replace operation
This operation is functionally the same as a remove
followed by an add
.
The following sample patch document sets the value of CustomerName
and replaces Orders[0]
with a new Order
object:
JSON
[
{
"op": "replace",
"path": "/customerName",
"value": "Barry"
},
{
"op": "replace",
"path": "/orders/0",
"value": {
"orderName": "Order2",
"orderType": null
}
}
]
Then we got the result:
4. The move operation
- If
path
points to an array element: copies from
element to location of path
element, then runs a remove
operation on the from
element.
- If
path
points to a property: copies value of from
property to path
property, then runs a remove
operation on the from
property.
- If
path
points to a nonexistent property:
- If the resource to patch is a static object: the request fails.
- If the resource to patch is a dynamic object: copies
from
property to location indicated by path
, then runs a remove
operation on the from
property.
The following sample patch document:
- Copies the value of
Orders[0].OrderName
to CustomerName
.
- Sets
Orders[0].OrderName
to null.
- Moves
Orders[1]
to before Orders[0]
.
JSON
[
{
"op": "move",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "move",
"from": "/orders/1",
"path": "/orders/0"
}
]
Then we got the result:
5. The copy operation
This operation is functionally the same as a move
operation without the final remove
step.
The following sample patch document:
- Copies the value of
Orders[0].OrderName
to CustomerName
.
- Inserts a copy of
Orders[1]
before Orders[0]
.
JSON
[
{
"op": "copy",
"from": "/orders/0/orderName",
"path": "/customerName"
},
{
"op": "copy",
"from": "/orders/1",
"path": "/orders/0"
}
]
Then we got the result:
6. The test operation
If the value at the location indicated by path
is different from the value provided in value
, the request fails. In that case, the whole PATCH request fails even if all other operations in the patch document would otherwise succeed.
The test
operation is commonly used to prevent an update when there's a concurrency conflict.
The following sample patch document has no effect if the initial value of CustomerName
is "John", because the test fails:
JSON
[
{
"op": "test",
"path": "/customerName",
"value": "Nancy"
},
{
"op": "add",
"path": "/customerName",
"value": "Barry"
}
]
Then we got the result:
D: Patch Implementation in ASP.NET Core Web API, for Database
Now we run PATCH for View Models/DTOs from memory, and then we need to apply those patches back onto a database. Years ago, people use Automapper to do the job, and now with .NET Core 5.0 (at least), we can do it just by the Microsoft code. We will demo a short sample here.
It seems even I just introduce the conclusion, this article wil bel still too long, I'd rather leave it to a new article as part II of this one.
Summary
Thia article described the PATCH concept briefly, and discussed the 6 PATCH operations associated with a ASP.NET Core Web API App. We will discuss the PATCH Implementation in ASP.NET Core Web API, for Database, in the next article of part II of this article.
Reference