Building a basic REST AspNet Core Example - Part 3
In the previous part 1 & 2 we build an invoices controller with support for POST/PUT/GET/PATCH http methods. In this one we will add HTTP HEAD method and ETag 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 at github.
Adding HTTP HEAD support
You can read about the HEAD verb in the HTTP protocol. But the part we are interested in is
The HEAD method is identical to GET except that the server MUST NOT send a message body in the response (i.e., the response terminates at the end of the header section). The server SHOULD send the same header fields in response to a HEAD request as it would have sent if the request had been a GET, except that the payload header fields MAY be omitted
The verb is useful for existence checking, validation that you have the latest version etc. Here we will use it for existance checking to start with. We will omit the Content-Length header since it is allowed to do so, and our use case does not rely on it.
So let’s add a minimum implementation to the InvoicesController
Until now we have not really cared if our PUT call tries to update something even though it may have a stale view of the resource. The ETag header can help us here. We can specify the current entity version on GET response with an ETag header. When we do a PUT we can then specify the “If-match” header with the ETag. Thus we will only update the resource if the ETag matches. But to use the ETag we would want to use it on most Http verbs. The example below uses the invoice version as the ETag - in principle this should have been an “opaque” value instead.
GET
We should return an ETag in the response when a GET request is made. It could be done like this (somewhat clumpsy)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[HttpGet("{id}", Name = "GetInvoice")]
public IActionResult Get(string id)
{
var invoice = invoiceRepository.Get(id);
if (invoice == null)
{
return NotFound();
}
var responseHeaders = Response.GetTypedHeaders();
responseHeaders.ETag = new EntityTagHeaderValue($"\"{invoice.Version}\"");
return Ok(getInvoiceMapper.ToModel(invoice));
}
Please note the invoice has been extended with a Version property that is changed when the update method of the repository is called.
So now when we make a request the response headers can look like this
We should also be able to handle the ETag if specified in the request. It will make sense to implement support for If-none-match header. We will return a new entity if the ETag does not match the current version of the invoice. If it matches we will return HTTP Not Modified.
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
28
[HttpGet("{id}", Name = "GetInvoice")]
public IActionResult Get(string id)
{
var invoice = invoiceRepository.Get(id);
if (invoice == null)
{
return NotFound();
}
var currentETag = new EntityTagHeaderValue($"\"{invoice.Version}\"");
and if it is current we get these response headers
1
2
3
HTTP/1.1 304 Not Modified
Date: Mon, 12 Dec 2016 17:28:02 GMT
Server: Kestrel
PUT/PATCH
Here we are going to return HTTP status Precondition Failed (415) if an ETag is provided and it does not match. The request should have the If-Match header added to it. Here is PUT
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[HttpPut("{id}")]
public IActionResult Put(string id, [FromBody] UpdateInvoice updateInvoice)
{
if (updateInvoice == null)
{
return BadRequest();
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (!invoiceRepository.Exists(id))
{
return NotFound();
}
if (IfMatchIsInvalid(invoiceRepository.GetCurrentVersion(id)))