Skip to content

Commit 5a9b06e

Browse files
committed
Added Mike's sample and updated action method section
1 parent edd8b74 commit 5a9b06e

20 files changed

+374
-232
lines changed

aspnetcore/web-api/jsonpatch.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,38 +59,46 @@ dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease
5959

6060
This package provides a <xref:Microsoft.AspNetCore.JsonPatch.JsonPatchDocument%601> class to represent a JSON Patch document for objects of type `T` and custom logic for serializing and deserializing JSON Patch documents using <xref:System.Text.Json>. The key method of the <xref:Microsoft.AspNetCore.JsonPatch.JsonPatchDocument%601> class is <xref:Microsoft.AspNetCore.JsonPatch.JsonPatchDocument.ApplyTo(System.Object)>, which applies the patch operations to a target object of type `T`.
6161

62-
## Action method code
62+
## Controller Action method code applying JSON Patch
6363

6464
In an API controller, an action method for JSON Patch:
6565

6666
* Is annotated with the <xref:Microsoft.AspNetCore.Mvc.HttpPatchAttribute> attribute.
6767
* Accepts a <xref:Microsoft.AspNetCore.JsonPatch.JsonPatchDocument%601>, typically with [<xref:Microsoft.AspNetCore.Mvc.FromBodyAttribute>](xref:Microsoft.AspNetCore.Mvc.FromBodyAttribute).
6868
* Calls <xref:Microsoft.AspNetCore.JsonPatch.JsonPatchDocument.ApplyTo(System.Object)> on the patch document to apply the changes.
6969

70-
Here's an example:
70+
### Example Action method:
7171

72-
:::code language="csharp" source="~/web-api/jsonpatch/samples/3.x/api/Controllers/HomeController.cs" id="snippet_PatchAction" highlight="1,3,9":::
72+
:::code language="csharp" source="~/web-api/jsonpatch/samples/10.x/JsonPatchSample/Controllers/CustomerController.cs" id="snippet_PatchAction" highlight="1,2,14-19":::
7373

7474
This code from the sample app works with the following `Customer` and `Order` models:
7575

76-
:::code language="csharp" source="~/web-api/jsonpatch/samples/6.x/api/Models/Customer.cs":::
76+
:::code language="csharp" source="~/web-api/jsonpatch/samples/10.x/api/Models/Customer.cs":::
7777

78-
:::code language="csharp" source="~/web-api/jsonpatch/samples/6.x/api/Models/Order.cs":::
78+
:::code language="csharp" source="~/web-api/jsonpatch/samples/10.x/api/Models/Order.cs":::
7979

80-
The sample action method:
80+
The sample action method's key steps:
8181

82-
* Constructs a `Customer`.
83-
* Applies the patch.
84-
* Returns the result in the body of the response.
82+
* **Retrieve the Customer**:
83+
* The method retrieves a `Customer` object from the database `AppDb` using the provided id.
84+
* If no `Customer` object is found, it returns a `404 Not Found` response.
85+
* **Apply JSON Patch**:
86+
* The <xref:Microsoft.AspNetCore.JsonPatch.JsonPatchDocument.ApplyTo(System.Object)> method applies the JSON Patch operations from the patchDoc to the retrieved `Customer` object.
87+
* If errors occur during the patch application, such as invalid operations or conflicts, they are captured by an error handling delegate. This delegate adds error messages to the `ModelState` using the type name of the affected object and the error message.
88+
* **Validate ModelState**:
89+
* After applying the patch, the method checks the `ModelState` for errors.
90+
* If the `ModelState` is invalid, such as due to patch errors, it returns a `400 Bad Request` response with the validation errors.
91+
* **Return the Updated Customer**:
92+
* If the patch is successfully applied and the `ModelState` is valid, the method returns the updated `Customer` object in the response.
8593

86-
### Model state
94+
### Example error response:
8795

88-
The preceding action method example calls an overload of <xref:Microsoft.AspNetCore.JsonPatch.JsonPatchDocument.ApplyTo(System.Object)> that takes model state as one of its parameters. With this option, you can get error messages in responses. The following example shows the body of a 400 Bad Request response for a `test` operation:
96+
The following example shows the body of a `400 Bad Request` response for a JSON Patch operation when the specified path is invalid:
8997

9098
```json
9199
{
92100
"Customer": [
93-
"The current value 'John' at path 'customerName' != test value 'Nancy'."
101+
"The target location specified by path segment 'foobar' was not found."
94102
]
95103
}
96104
```
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
3+
4+
using App.Data;
5+
using App.Models;
6+
7+
namespace App.Controllers;
8+
9+
[ApiController]
10+
[Route("/api/customers")]
11+
public class CustomerController : ControllerBase
12+
{
13+
[HttpGet("{id}", Name = "GetCustomer")]
14+
public Customer Get(AppDb db, string id)
15+
{
16+
// Retrieve the customer by ID
17+
var customer = db.Customers.FirstOrDefault(c => c.Id == id);
18+
19+
// Return 404 Not Found if customer doesn't exist
20+
if (customer == null)
21+
{
22+
Response.StatusCode = 404;
23+
return null;
24+
}
25+
26+
return customer;
27+
}
28+
29+
// <snippet_PatchAction>
30+
[HttpPatch("{id}", Name = "UpdateCustomer")]
31+
public IActionResult Update(AppDb db, string id, [FromBody] JsonPatchDocument<Customer> patchDoc)
32+
{
33+
// Retrieve the customer by ID
34+
var customer = db.Customers.FirstOrDefault(c => c.Id == id);
35+
36+
// Return 404 Not Found if customer doesn't exist
37+
if (customer == null)
38+
{
39+
return NotFound();
40+
}
41+
42+
patchDoc.ApplyTo(customer, jsonPatchError =>
43+
{
44+
var key = jsonPatchError.AffectedObject.GetType().Name;
45+
ModelState.AddModelError(key, jsonPatchError.ErrorMessage);
46+
}
47+
);
48+
49+
if (!ModelState.IsValid)
50+
{
51+
return BadRequest(ModelState);
52+
}
53+
54+
return new ObjectResult(customer);
55+
}
56+
// </snippet_PatchAction>
57+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using Microsoft.AspNetCore.Http.HttpResults;
2+
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
3+
using Microsoft.EntityFrameworkCore;
4+
5+
using App.Data;
6+
using App.Models;
7+
8+
internal static class CustomerApi {
9+
public static void MapCustomerApi(this IEndpointRouteBuilder routes)
10+
{
11+
var group = routes.MapGroup("/customers").WithTags("Customers");
12+
13+
group.MapGet("/{id}", async Task<Results<Ok<Customer>, NotFound>> (AppDb db, string id) =>
14+
{
15+
return await db.Customers.Include(c => c.Orders).FirstOrDefaultAsync(c => c.Id == id) is Customer customer
16+
? TypedResults.Ok(customer)
17+
: TypedResults.NotFound();
18+
});
19+
20+
group.MapPatch("/{id}", async Task<Results<Ok<Customer>,NotFound,BadRequest, ValidationProblem>> (AppDb db, string id,
21+
JsonPatchDocument<Customer> patchDoc) =>
22+
{
23+
var customer = await db.Customers.Include(c => c.Orders).FirstOrDefaultAsync(c => c.Id == id);
24+
if (customer is null)
25+
{
26+
return TypedResults.NotFound();
27+
}
28+
if (patchDoc != null)
29+
{
30+
Dictionary<string, string[]>? errors = null;
31+
patchDoc.ApplyTo(customer, jsonPatchError =>
32+
{
33+
errors ??= new ();
34+
var key = jsonPatchError.AffectedObject.GetType().Name;
35+
if (!errors.ContainsKey(key))
36+
{
37+
errors.Add(key, new string[] { });
38+
}
39+
errors[key] = errors[key].Append(jsonPatchError.ErrorMessage).ToArray();
40+
});
41+
if (errors != null)
42+
{
43+
return TypedResults.ValidationProblem(errors);
44+
}
45+
await db.SaveChangesAsync();
46+
}
47+
48+
return TypedResults.Ok(customer);
49+
})
50+
.Accepts<JsonPatchDocument<Customer>>("application/json-patch+json");
51+
}
52+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using App.Models;
3+
4+
namespace App.Data;
5+
6+
public class AppDb : DbContext
7+
{
8+
public required DbSet<Customer> Customers { get; set; }
9+
10+
public AppDb(DbContextOptions<AppDb> options) : base(options)
11+
{
12+
}
13+
14+
protected override void OnModelCreating(ModelBuilder modelBuilder)
15+
{
16+
base.OnModelCreating(modelBuilder);
17+
18+
// Configure entity relationships here if needed
19+
modelBuilder.Entity<Customer>()
20+
.HasKey(c => c.Id);
21+
}
22+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using App.Models;
2+
using Microsoft.EntityFrameworkCore;
3+
4+
namespace App.Data;
5+
6+
public static class AppDbSeeder
7+
{
8+
public static async Task Seed(WebApplication app)
9+
{
10+
// Create and seed the database
11+
using (var scope = app.Services.CreateScope())
12+
{
13+
var services = scope.ServiceProvider;
14+
var context = services.GetRequiredService<AppDb>();
15+
context.Database.EnsureCreated();
16+
17+
if (context.Customers.Any())
18+
{
19+
return;
20+
}
21+
22+
Customer[] customers = {
23+
new Customer
24+
{
25+
Id = "1",
26+
Name = "John Doe",
27+
Email = "[email protected]",
28+
PhoneNumber = "555-123-4567",
29+
Address = "123 Main St, Anytown, USA"
30+
},
31+
new Customer
32+
{
33+
Id = "2",
34+
Name = "Jane Smith",
35+
Email = "[email protected]",
36+
PhoneNumber = "555-987-6543",
37+
Address = "456 Oak Ave, Somewhere, USA"
38+
},
39+
new Customer
40+
{
41+
Id = "3",
42+
Name = "Bob Johnson",
43+
Email = "[email protected]",
44+
PhoneNumber = "555-555-5555",
45+
Address = "789 Pine Rd, Elsewhere, USA"
46+
}
47+
};
48+
49+
await context.Customers.AddRangeAsync(customers);
50+
await context.SaveChangesAsync();
51+
}
52+
}
53+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.AspNetCore.JsonPatch.SystemTextJson" Version="10.0.0-preview.5.25277.114" />
11+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.5.25277.114" />
12+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0-preview.5.25277.114" />
13+
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0-preview.5.25277.114" />
14+
</ItemGroup>
15+
16+
</Project>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
@HostAddress = http://localhost:5221
2+
3+
GET {{HostAddress}}/openapi/v1.json
4+
Accept: application/json
5+
6+
###
7+
8+
GET {{HostAddress}}/api/customers/1
9+
Accept: application/json
10+
11+
###
12+
13+
PATCH {{HostAddress}}/api/customers/1
14+
Content-Type: application/json-patch+json
15+
Accept: application/json
16+
17+
[
18+
{
19+
"op": "replace",
20+
"path": "/email",
21+
"value": "[email protected]"
22+
}
23+
]
24+
25+
###
26+
27+
# Error response
28+
29+
PATCH {{HostAddress}}/api/customers/1
30+
Content-Type: application/json-patch+json
31+
Accept: application/json
32+
33+
[
34+
{
35+
"op": "add",
36+
"path": "/foobar",
37+
"value": 42
38+
}
39+
]
40+
41+
###
42+
### Minimal API requests
43+
###
44+
45+
GET {{HostAddress}}/customers/1
46+
Accept: application/json
47+
48+
###
49+
50+
PATCH {{HostAddress}}/customers/1
51+
Content-Type: application/json-patch+json
52+
Accept: application/json
53+
54+
[
55+
{
56+
"op": "replace",
57+
"path": "/email",
58+
"value": "[email protected]"
59+
}
60+
]
61+
62+
###
63+
64+
# Error response
65+
66+
PATCH {{HostAddress}}/customers/1
67+
Content-Type: application/json-patch+json
68+
Accept: application/json
69+
70+
[
71+
{
72+
"op": "add",
73+
"path": "/foobar",
74+
"value": 42
75+
}
76+
]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace App.Models;
2+
3+
public class Customer
4+
{
5+
public string Id { get; set; }
6+
public string? Name { get; set; }
7+
public string? Email { get; set; }
8+
public string? PhoneNumber { get; set; }
9+
public string? Address { get; set; }
10+
public List<Order>? Orders { get; set; }
11+
12+
public Customer()
13+
{
14+
Id = Guid.NewGuid().ToString();
15+
}
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace App.Models;
2+
3+
public class Order
4+
{
5+
public string Id { get; set; }
6+
public DateTime? OrderDate { get; set; }
7+
public DateTime? ShipDate { get; set; }
8+
public decimal TotalAmount { get; set; }
9+
10+
public Order()
11+
{
12+
Id = Guid.NewGuid().ToString();
13+
}
14+
}

0 commit comments

Comments
 (0)