Building a basic REST AspNet Core Example - Part 2

In the last post we made the first step at a REST Api. In this post we will improve it by adding HTTP PATCH/OPTIONS support. I’ll repeat the disclamer - It is going to be pretty naive and basic, but I will add to it in future posts (hopefully). The source code for this post is here.

Adding HTTP PATCH support

With PUT we have to supply the complete invoice in order to update it. That may not be what you want, so HTTP PATCH (rfc5789) can help us here by allowing partial resource modification.

The PATCH method requests that a set of changes described in the
request entity be applied to the resource identified by the Request-
URI. The set of changes is represented in a format called a “patch
document” identified by a media type

For JSON in .NET core this is implemented using JavaScript Object Notation (JSON) Patch. The media type is “application/json-patch+json”. From Json Patch you can read

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
A JSON Patch document is a JSON [RFC4627] document that represents an
array of objects. Each object represents a single operation to be
applied to the target JSON document.
The following is an example JSON Patch document, transferred in a HTTP PATCH request:
PATCH /my/data HTTP/1.1
Host: example.org
Content-Length: 326
Content-Type: application/json-patch+json
If-Match: "abc123"
[
{ "op": "test", "path": "/a/b/c", "value": "foo" },
{ "op": "remove", "path": "/a/b/c" },
{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
{ "op": "replace", "path": "/a/b/c", "value": 42 },
{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]

So { “op”: “replace”, “path”: “/a/b/c”, “value”: 42 } tells us that the operation should replace the target value with 42. The path is a JSON Pointer. If you want to take a look at the aspnet source code then look here. From the unit test you can also see that the “test” operation is not supported.

So let us extend our InvoicesController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[HttpPatch("{id}")]
public IActionResult Patch(string id, [FromBody] JsonPatchDocument<UpdateInvoice> patchDocument)
{
if (patchDocument == null)
{
return BadRequest();
}
var invoice = invoiceRepository.Get(id);
if (invoice == null)
{
return NotFound();
}
var updateInvoice = updateInvoiceMapper.ToModel(invoice);
patchDocument.ApplyTo(updateInvoice, ModelState);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var updatedDomainInvoice = updateInvoiceMapper.ToDomain(updateInvoice, id);
invoiceRepository.Update(updatedDomainInvoice);
return NoContent();
}

In the method our input is now JsonPatchDocument meaning it is an UpdateInvoice model we perform the operations on. Again we start out with a validation if the input is malformed. Next we handle the case where the invoice is not found. Since we perform the operations against an instance of UpdateInvoice here, then we must construct that instance first from the domain model. So ToModel transforms the loaded entity to our model representation. Then the patch operations are applied using patchDocument.ApplyTo. We also pass in the ModelState so that the validation will still kick in. After ApplyTo we check the ModelState. Last step it to update the invoice using the repository.

HTTP Options

To provide a more complete REST Api we can choose to implement the HTTP OPTIONS verb.

The OPTIONS method represents a request for information about the communication options available on the request/response chain identified by the Request-URI. This method allows the client to determine the options and/or requirements associated with a resource, or the capabilities of a server, without implying a resource action or initiating a resource retrieval.

Using it we can determine which other Http Verbs that are available at a given uri. We can use this to eg. remove Http Put/Patch methods when no instance exists. The methods that are available should be specified in the allow header

Allow: GET, POST, PUT

So let’s add the first OPTIONS method for the /invoices uri

1
2
3
4
5
6
[HttpOptions]
public IActionResult Options()
{
Response.Headers.Add("Allow", string.Join(",", HttpVerbs.Options, HttpVerbs.Post));
return NoContent();
}

Now when we make a OPTIONS request we get

1
2
3
HTTP/1.1 204 No Content
Date: Mon, 12 Dec 2016 10:22:07 GMT
Allow: OPTIONS,POST

basically saying all we can do is create a new invoice.

When we call HTTP OPTIONS on an invoice we should return OPTIONS,GET,PUT,PATCH if the invoice exists. If not then “OPTIONS, GET”. I include GET since it can return a 404 as the response.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[HttpOptions("{id}")]
public IActionResult OptionsForInvoice(string id)
{
if (!invoiceRepository.Exists(id))
{
Response.Headers.Add("Allow", string.Join(",",
HttpVerbs.Options,
HttpVerbs.Get));
}
else
{
Response.Headers.Add("Allow", string.Join(",",
HttpVerbs.Options,
HttpVerbs.Get,
HttpVerbs.Put,
HttpVerbs.Patch));
}
return NoContent();
}

Wrap up

That’s it for this time, but we are still not there so other posts will follow.