Building a basic REST AspNet Core Example - Part 1

In this post we will start on an app that exposes a REST web api for an invoice. Disclaimer - It is going to be pretty naive and basic, but I will add to it in further posts (hopefully). So actually we will for now go no higher in richardson’s maturity model than level 2.

My inspiration for this blog is attending skillsmatters “Fast-Track to RESTful Microservices” course with Jim Webber. It is highly based on the book REST in Practice: Hypermedia and Systems Architecture. Although the book is now old it still contains good information (code examples are naturally outdated) - so read it if you need extensive REST information, but can live with not all is up to date.

The case here is creating, updating and getting an invoice along the lines of these user stories.

1
2
3
As a <business user>
I want <to be able to create an invoice for a customer>
So that <we can facilitate sales>
1
2
3
As a <business user>
I want <to be able to update an invoice>
So that <it can be created in steps>
1
2
3
As a <business user>
I want <to be able to view the invoice>
So that <I can answer questions related to it>

Getting a list of invoices, searching etc is too advanced for now.

The server code will be .NET core aspnet web api (and for the fun I’m developing on ubuntu with Jetbrains Rider). It will be without a real database backing it for now - since the focus is mostly on the REST part. It does take a pretty naive approach on design of the API, but hopefully I will get to a post where this part is evolved.

You can find the code discussed at github in the 1_InvoiceApi folder.

The Domain

We will call our bounded context finance, and the primary class will be the invoice. We will start with this naive definition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace Finance.Domain.Domain
{
public sealed class Invoice
{
public Invoice(string id, DateTimeOffset invoiceDate,
DateTimeOffset dueDate,
InvoiceCustomer invoiceCustomer,
IEnumerable<InvoiceLine> lines)
{
InvoiceDate = invoiceDate;
DueDate = dueDate;
Customer = invoiceCustomer;
Lines = lines;
Id = id;
}
public string Id { get; set; }
public DateTimeOffset InvoiceDate { get; }
public DateTimeOffset DueDate { get; }
public InvoiceCustomer Customer { get; }
public IEnumerable<InvoiceLine> Lines { get; }
public Amount SubTotal => new Amount(Lines.Sum(l => l.Total.Value));
}
}

The customer and lines are not important for understanding at present time.

The repository

The invoice is accompanied with an InvoiceReposity class implementing IRepository<Invoice> that looks like this

1
2
3
4
5
6
7
public interface IRepository<T>
{
T Get(string id);
void Update(T instance);
T Create(T instance);
bool Exists(string id);
}

for now we have a list of 1000 prepopulated invoices with id 1-1000. The repository is a simple dictionary.

Looking for the API

We will center the API around what the business does - that is its domain and the mentioned user stories. With Level 1 in the REST maturity level with introduce resource representations. We use urls to differentiate between these resource representations. So in our case it will be an invoice resource representation. Remember the resource representation does not have to match our domain model. It will be designed from what information we wish to conway, and we may have multiple resource representations for the same domain invoice.

So since we are going for a CRU(D) implementation here our invoice API can look like this

Http Verb Uri Usage
GET invoices/{id} Get invoice representation
POST invoices Create a new invoice
PUT invoices/{id} Update an existing invoice

Actually we are already getting a good start on Level 2 Http in the maturity model. We are using the Http protocol verbs, and we must make sure to use status codes correctly, content types and all the other good stuff (safe verbs and idempotency). Now you could ask where the GET invoice/ is. For now it is not there - remember we had 1000 invoices prepopulated - it will need a little more design.

Deciding on a media type

We need to chose a media type for the http calls. Since json is “in” at the moment we could choose application/json. However we would like to have something less general purpose to be able to tell the client about the expected processing model (like introduce hypermedia later). So we create a media type in the vendor tree

application/vnd.restexample.finance+json

assuming we as the owner are called “restexample”. We also added the application name “finance”, but should probably not break it further down, since it will put demands on the client if we have multiple media types. The suffix +json now tells what the underlying representation is.

So in order for us to return any information the client must specify the http header

Accept: application/vnd.restexample.finance+json

or any variant such as */*, application/*.

To enforce the media type we add the following to the Startup class.

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
42
43
44
45
46
47
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(LimitFormattersToThisApplication);
}
private void LimitFormattersToThisApplication(MvcOptions options)
{
options.RespectBrowserAcceptHeader = true;
options.ReturnHttpNotAcceptable = true;
var customMediaType = new MediaTypeHeaderValue(ApiDefinition.ApiMediaType);
options.SetInputMediaType(customMediaType);
options.SetOutputMediaType(customMediaType);
}
public static class MvcOptionsExtensions
{
public static void SetInputMediaType(this MvcOptions options, MediaTypeHeaderValue mediaType)
{
var supportedInputMediaTypes = options
.InputFormatters
.OfType<JsonInputFormatter>()
.First()
.SupportedMediaTypes;
SetAllowedMediaType(mediaType, supportedInputMediaTypes);
}
public static void SetOutputMediaType(this MvcOptions options, MediaTypeHeaderValue mediaType)
{
var supportedOutputMediaTypes = options
.OutputFormatters
.OfType<JsonOutputFormatter>()
.First()
.SupportedMediaTypes;
SetAllowedMediaType(mediaType, supportedOutputMediaTypes);
}
private static void SetAllowedMediaType(MediaTypeHeaderValue mediaType,
MediaTypeCollection supportedInputMediaTypes)
{
supportedInputMediaTypes.Clear();
supportedInputMediaTypes.Add(mediaType);
}
}

The extension methods sets the output and input media type. Note ReturnHttpNotAcceptable is set to true in order to return

406 Not Acceptable

when the reponse format requested in the accept header does not match our custom media type.

Invoice resource representation

GET

For now we will use this invoice representation for GET that matches the domain representation except for some value object representations in the domain (DDD term)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"id": "1000",
"invoiceDate": "2016-12-05T00:00Z",
"dueDate": "2016-12-12T00:00Z",
"subTotal": 9050,
"customer": {
"name": "Microsoft Development Center",
"addressLines": ["Kanalvej 8", "2800 Kongens Lyngby"]
},
"lines": [
{
"lineNumber": 1,
"description": "consultancy",
"quantity": 10,
"itemPrice": 905,
"total": 9050
}
]
}

That raises the question of what happens to “total” values on POST or PUT. The answer is we need a different representation without these fields.

PUT/POST

Besides removing the total values in the resource representation we also remove the id, ie. we will not let it be up to the client to set it (could be a valid option).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"invoiceDate": "2016-12-05T00:00Z",
"dueDate": "2016-12-12T00:00Z",
"customer": {
"name": "Microsoft Development Center",
"addressLines": ["Kanalvej 8", "2800 Kongens Lyngby"]
},
"lines": [
{
"lineNumber": 1,
"description": "consultancy",
"quantity": 10,
"itemPrice": 905
}
]
}

The Invoice controller

For now we are not going all in on DDD with a separate application service layer, but rather survive with that code in the controller.

Get

So here is the GET implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Route("invoice")]
public sealed class InvoicesController : Controller
{
...
[HttpGet("{id}", Name = "GetInvoice")]
public IActionResult Get(string id)
{
var invoice = invoiceRepository.Get(id);
if (invoice == null)
{
return NotFound();
}
return Ok(mapper.ToModel(invoice));
}
...

We use a literal in the Route attribute to avoid issues with refactoring. If we cannot find the invoice then we return a 404 Not found. Else we return the invoice. It is mapped to this GET model. Notice the HttpGet specifies a Name. This is used in the Post action later.

1
2
3
4
5
6
7
8
9
public sealed class GetInvoice
{
public DateTimeOffset InvoiceDate { get; set; }
public DateTimeOffset DueDate { get; set; }
public GetInvoiceCustomer Customer { get; set; }
public decimal SubTotal { get; set; }
public IEnumerable<GetInvoiceLine> Lines { get; set; }
public string Id { get; set; }
}

I am sure GetInvoice will cause some discussion for now. You could do without different representations for the Http verb’s, but then that would impact on the validation etc. That is the reason behind the choice here as you will see in a moment on POST.

POST

In post we have more to take into account.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[HttpPost]
public IActionResult Post([FromBody] UpdateInvoice updateInvoice)
{
if (updateInvoice == null)
{
return BadRequest();
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var createdInvoice = invoiceRepository.Create(updateInvoiceMapper.ToDomain(updateInvoice));
return CreatedAtRoute("GetInvoice",
new { id = createdInvoice.Id },
getInvoiceMapper.ToModel(createdInvoice));
}

First of all the request could be malformed in such a way that updateInvoice is null. We will return BadRequest then.

Next is to check if the model is valid. This is one of the reasons for having UpdateInvoice, UpdateInvoiceLine, UpdateInvoiceCustomer models where we decorate with data annotations like shown here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public sealed class UpdateInvoice
{
public DateTimeOffset InvoiceDate { get; set; }
public DateTimeOffset DueDate { get; set; }
public UpdateInvoiceCustomer Customer { get; set; }
public IEnumerable<UpdateInvoiceLine> Lines { get; set; }
}
public sealed class UpdateInvoiceLine
{
public int LineNumber { get; set; }
public decimal Quantity { get; set; }
public decimal ItemPrice { get; set; }
[Required(ErrorMessage = "Invoice line description must be specified")]
[MaxLength(500, ErrorMessage = "Invoice line description is too long")]
public string Description { get; set; }
}

If everything goes well we will return the newly created invoice with a 201 and a Location header for the new resource like shown here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
HTTP/1.1 201 Created
Date: Thu, 08 Dec 2016 12:40:35 GMT
Transfer-Encoding: chunked
Content-Type: application/vnd.restexample.finance+json; charset=utf-8
Location: http://localhost:5000/invoice/1002
{
"invoiceDate":"2016-12-05T00:00:00+00:00",
"dueDate":"2016-12-12T00:00:00+00:00",
"customer":{
"name":"Microsoft Development Center",
"addressLines":["Kanalvej 8","2800 Kongens Lyngby"]
},
"subTotal":0.0,
"lines":[
{"lineNumber":1,"description":"test","quantity":10.0,"itemPrice":1.0,"total":10.0}
],
"id":"1002"
}

Note that when calling POST we have to add the headers

Accept: application/vnd.restexample.finance+json
Content-Type: application/vnd.restexample.finance+json

However the code here in the POST is not pretty when it comes to DRY, but that will be a topic for a later blog.

PUT

With Put we wish to update the invoice. We must supply the full payload for updating. We will in a later blog look at the Patch verb.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[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();
}
invoiceRepository.Update(updateInvoiceMapper.ToDomain(updateInvoice, id));
return NoContent();
}

The two first validations are simular to Post. Then we check if the invoice exists and return NotFound if not. If we can update then we return NoContent (the client has the invoice already).

Wrap-up

That is it for the most basic api for now. But we can do much more as we will see in later posts (hopefully).