We use 4 standard HTTP methods, and usually each REST controller has at least the following actions:
GET /products?title=bread
- Returns
List<ProductListDto>
,ProductListDto
usually contains only a few fields (that are required to show the search results) - This method usually accepts some search parameters (e.g.
title
) - This method usually also accepts some sorting/paging parameters (e.g. inherits from
PagedRequestDto
)
- Returns
GET /products/1
- returnsProductDto
for Product with givenId
(1).ProductDetailsDto
usually contains more fields thanProductListDto
.POST /products
- accepts aCreateProductDto
in request body. Creates a new product and returns aProductDto
.PATCH /products/1
- patches aProduct
with given id, read [details below](#HTTP PATCH implementation).DELETE /products/1
- deletes the product with given id
While we generally prefer the REST, sometimes it makes sense to refrain from it for certain workflows. For example, you might want to implement a POST /products/1/archive
method to archive a certain product (i.e. make it non-active). While it's possible to implement it via REST (e.g. by adding isArchived
property to GET and PATCH methods), separate method might make more sense.
You could use the rule of thumb: if it's likely that the method might need a different Permissions (role/claim/access right), then it makes sense to implement it in a separate method (e.g. archiving a product might only be available for Admin user).
The idea of http PATCH method, is that it changes the object (just like a PUT method), but only changes the values that are actually passed, without setting all others to null
.
For example, if we have a Product
with the following values:
{
"title": "Bread",
"productType": 1,
"lastStockUpatedAt": "2022-01-01"
}
and we send the following PATCH request:
{
"lastStockUpdatedAt": "2022-02-01"
}
then only lastStockUpdatedAt
field will actually be changed (by definition, the PUT
method completely replace the resource, i.e. it should set title
and productType
to null
in this case).
The behaviour of PATCH method is actually better, agile and backwards compatible. That is, if frontend wants to change a single field only, then the most intuitive way is to issue a PATCH request passing a single field only.
The easiest thing is to actually check the sources of PatchProductDto and Patch method of ProductService.
In short, you have to inherit your PatchDto
from PatchRequest<T>
. Then you define the properties that you would like to be patched via this method (you could define a subset of Entity properties, for example, you probably wouldn't want to change the Password
field of a User
via PATCH method).
public class PatchProductDto : PatchRequest<Product>
{
[MinLength(3)]
public string Title { get; set; }
public ProductType ProductType { get; set; }
public DateOnly LastStockUpdatedAt { get; set; }
}
After that you can call product.Update(productPatchDto)
, and it will change those properties of product
that were passed in productPatchDto
.
For more details you could check an implementation of Update
extension method in PartialUpdateHelper.
The base PatchRequest class is needed for 2 things:
- To distinguish when property value is intentionally set to
null
(e.g.{ "title": null }
) and when property value is not set (e.g.{}
).- By default in both cases the value of
Title
property will benull
. - But in case of
{ "title": null }
we want to change the value oftitle
tonull
, in second case, we don't want to change it at all. - So, we customize the JSON deserialization procedure, and
PatchRequest
has a special methodIsFieldPresent
that returnstrue
if the field was present in a request (first case), andfalse
if it wasn't (second case).
- By default in both cases the value of
- There's a test that verifies, that PatchDtos only contain the properties that exist in the Entity. That's helpful to avoid issues when you rename entity properties.
- To add an exception for certain Dtos/fields just modify the test
PatchRequest_AllFieldsMatch
in BasicApiTests.cs.
- To add an exception for certain Dtos/fields just modify the test
P.S. There's also a blog post which talks about the same thing in russian.