DEV Community

Cover image for Episode 5 - JSON API using ASP.NET Core, Docker &  MongoDB - Modelling, Controller and Unit Tests Part I - BookStore
Gareth Bradley
Gareth Bradley

Posted on • Originally published at garfbradaz.github.io on

Episode 5 - JSON API using ASP.NET Core, Docker & MongoDB - Modelling, Controller and Unit Tests Part I - BookStore

Previously on Dcoding

In Episode 4 I set up our docker-compose files to allow us to knit together our application and the services it will be use. Today’s episode is focusing on Modelling, Controller and Unit Tests for the BookStore Object.

Here is a reminder on the sample User Stories / Epics for phase 1.

As a book store_I can _add our store to the database_So_ we can be accessible

As a book store_I can _add our inventory to our database_So_ we can expose our inventory

As a book store_We can _update a books stock level_For_ an accurate stock level

As a API consumer_I can look up a _stores addressSo we know where to buy a book

As a API consumer_I can look up a _bookSo we can get a list of stores who sell a_book_

As book store IT Security_We can add _API Keys to the API_For_ API Consumers to use when querying the API

Dependency on MongoDB

Our application will ultimately be using MongoDB as its back end data store. We need to take a dependency on the MongoDB driver. This will allow us to communicate with and use MongoDB.

Using your shell of choice, change directory to ../src/api directory. To recap, our directory structure is:

.
    ├── src
    | ├── api
    | |
    | ├── BookStoreApp.WebApi.csproj
    | ├── Dockerfile
    ├── tests
    | ├── integration
    | ├── unit
    | ├── BookStore.Tests.csproj
    ├── docker

Note: Something has changed, can you guess what? Yep that’s right, I have renamed the .csproj to BookStoreApp.WebApi.csproj instead of BookStore.WebApi.csproj.

Now run the following command, which will add the latest Nuget package to your api project.

dotnet add package MongoDB.Driver

We will be returning to MongoDB once we have the unit test infrastructure set up in the next episode.

BookStore

Modelling

The first of our models (the M in M VC), will represent our BookStore. A simple Plain old C# (POCO) to represent this:

/// <summary>
    /// BookStore POCO.
    /// </summary>
    public class BookStore
    {
        [BsonId]
        public ObjectId Id {get; set;}
        public string Name {get;set;}
        public string AddressLine1 {get;set;}
        public string AddressLine2 {get;set;}
        public string AddressLine3 {get;set;}
        public string City {get;set;}
        public string PostCode {get;set;}
        public string TelephoneNumber {get;set;}

        /// <summary>
        /// Default constructor
        /// </summary>
        public BookStore()
        {
            this.Id = ObjectId.GenerateNewId();
        }
    }

I added a Model folder to my ../api folder where this was added. MongoDB uses a serialization format standard called BSON. BSON has the idea of types and one of those types is ObjectId which represents a unique id for the record. You decorate the Id property with [BsonId] attribute, to inform the MongoDB driver what field is your BsonId field.

I have also created a default constructor that sets that Id up. Now we have created the Models directory and structure is now the following:

.
    ├── src
    | ├── api
    | ├── Models
    | ├── BookStore.cs
    | ├── BookStoreApp.WebApi.csproj
    | ├── Dockerfile
    ├── tests
    | ├── integration
    | ├── unit
    | ├── BookStore.Tests.csproj

Controller

Now we need to create a BookStore controller. A controller is the C in MV C pattern. The controller will handle the HTTP requests that come in using Actions. The HTTP requests are mapped to Actions via the routing pipeline of MVC. For WebAPIs, they map to specific HTTP methods like GET, POST, PUT etc.

ASP.NET expects you to follow conventions when creating your own Controllers:

  • The Controller class name always ends with Controller.
  • The Controller class should reside in a folder called Controllers.
  • The Controller class should inherit from Microsoft.AspNetCore.Mvc.ControllerBase (WebAPI) / Microsoft.AspNetCore.Mvc.Controller (ASP.NET MVC Apps with Views).

When you do a dotnet new webapi the templates include a standard ValuesController.cs that lives in a Controllers folder. Create a new BookStoreController.cs file in there:

.
    ├── src
    | ├── api
    | ├── Controllers
    | ├── BookStoreController.cs
    | ├── Models
    | ├── BookStore.cs
    | ├── BookStoreApp.WebApi.csproj
    | ├── Dockerfile
    ├── tests
    | ├── integration
    | ├── unit
    | ├── BookStore.Tests.csproj

To this file I have added the following:

[Route("api/[controller]")]
    [ApiController]
    public class BookStoreController : ControllerBase
    {
        // GET api/values
        [HttpGet]
        public async Task<ActionResult> Get()
        {
            var bookStore = new List<BookStore>{
                new BookStore {
                    Name = "Waterstones",
                    AddressLine1 = "The Dolphin & Anchor",
                    AddressLine2 = "West Street",
                    City = "Chichester",
                    PostCode = "PO19 1QD",
                    TelephoneNumber = "01234 773030"
                }
            };
            return await Task.Run(() => new JsonResult(bookStore));
        }
    }

Its a simple GET method that just returns a hard coded BookStore object. For today’s episode we are just concentrating on getting our unit test infrastructure up and running so we can ignore PUT, POST and DELETE until my next episode.

Unit Tests

Before we move forward we need to use the dotnet tooling to add a project reference to our tests project. Change directory to tests\unit and do the following to add a reference:

dotnet add reference ..\..\src\api\BookStoreApp.WebApi.csproj

I have also added two basic test methods to cover our new GET method in oir controller:

public class ControllerTests
    {
        [Fact]
        public async Task BookStoreController_Get_Should_Return_ActionResult()
        {
            //Arrange
            var controller = new BookStoreController();

            //Act
            var result = await controller.Get();

            //Assert
            Assert.IsType<JsonResult>(result);
        }

        [Fact]
        public async Task BookStoreController_Get_Should_Return_Correct_BookStore_Data()
        {
            //Arrange
            var controller = new BookStoreController();

            //Act
            var result = await controller.Get();
            var json = result.ToJson<BookStore>();

            //Assert
            Assert.True(json[0].Name == "Waterstones",$"Assert failed, received {json[0].Name} ");
            Assert.True(json[0].PostCode == "PO19 1QD",$"Assert failed, received {json[0].PostCode} ");
            Assert.True(json[0].TelephoneNumber == "01234773030",$"Assert failed, received {json[0].TelephoneNumber} ");
        }
    }

Before we start integrating with Docker, we can test using (you guessed it), the dotnet tooling. Make sure you are in the tests\unit directory and run:

dotnet restore
dotnet test

The test will set off a dotnet build first then ran our XUnit tests. One test should fail and this will be outputted similar to this:

Total tests: 2. Passed: 1. Failed: 1. Skipped: 0.

Test Run Failed.

Test execution time: 2.6340 Seconds

The error was intentional, there should of been a space in between "01234773030", I fixed this:

Assert.True(json[0].TelephoneNumber == "01234 773030",$"Assert failed, received {json[0].TelephoneNumber} ");

Re-run the tests and everything should now be green:

Total tests: 2. Passed: 2. Failed: 0. Skipped: 0.

Test Run Successful.

Test execution time: 3.1173 Seconds

Now we have a project with some basic logic within a controller, and a very basic model. We have also started creating some tests to cover this code. Now we need to build and run the application. We could at this stage use the normal route for that, but as these episodes include Docker , lets integrate what we know into a docker pipeline.

Docker

Firstly in the root of the project create a new Docker file. Your project structure should look like this now:

.
    ├── src
    ├── tests
    ├── Dockerfile

Here we will create a multi-stage Dockerfile that will restore, build and run our tests. Here is the first stage:

FROM microsoft/dotnet:2.2-sdk AS build-env
WORKDIR /app

COPY src/api/BookStoreApp.WebApi.csproj ./src/api/
RUN dotnet restore ./src/api/BookStoreApp.WebApi.csproj
COPY tests/unit/BookStore.Tests.csproj ./tests/unit/
RUN dotnet restore ./tests/unit/BookStore.Tests.csproj

COPY . .

You should be familiar with this now from previous episodes. This time take note that we are copying the unit test project files in to the build context. Also note we need to keep the same directory structure as before, because we added a dotnet add reference previously into the test project. If the directories didn’t match we would get build errors.

Run the following:

docker build -t test-ep5 .

Now do a docker image and you will see a massive image there:

test-5 image

Docker has a way of managing this, meet .dockerignore.

.dockerignore

This file behaves similarly to a .gitignore. It tells docker which files to not copying in during a build. So how do you know which files to ignore, well I learnt a good trick from Wes Higbee by passing in a ls alR to list out your directories. Run the following:

docker run --rm test-ep5 ls -alR

This will list out your containers file system, and you can easily see what is copied in. So things like .vscode, .git folders and bin directories. None of this stuff is needed during the build stage of this multi stage Dockerfile, so lets exclude it, using similar glob patterns you can use in .gitignore files. Add a .dockerignore file to your root:

.
    ├── src
    ├── tests
    ├── Dockerfile
    ├── .dockerignore

Then add the following, excluding files and artefacts that are not needed for a restore and build. We exclude things like our docker folder and .ps1 scripts we have been using. Plus the README.md and dockerfiles.

**/.vscode/
**/.git/
docker/
**/bin/
**/obj/
**/.dockerignore/
**/Dockerfile*
**/docker-compose*.yml/
run.ps1
clean.ps1
README.md

Re-run:

docker build -t test-ep5 .
 docker run --rm test-ep5 ls -alR

You will see a cleaner build plus about 100MB less space than previously:

test-5 image

Unit Tests in Docker

So now we have pruned our Image, we can add our tests to our Dockerfile. Add the following into your Dockerfile:

RUN dotnet test tests/unit/BookStore.Tests.csproj
RUN dotnet publish src/api/BookStoreApp.WebApi.csproj -o /publish

We run what we ran on the commandline earlier (see no magic). This will run our unit tests, then if they pass, we will publish a new release ready for our 2nd stage to use, by running dotnet publish and outputting to a new /publish directory.

Run again using the following and see your test success!

docker build -t test-ep5 .

If these unit tests fail, we will see the same as we did previously, so you can see how this all ties in. And just ecause we run our tests in docker, dotnet is still behaving the same. No docker magic.

Running your application

In previous episodes we had a multi stage Dockerfile with a 2nd stage that runs the application itself. No different here, our 2nd stage allows us to run our application.

NB: Once we have added this, we cannot use our “ls aLR” trick Wes taught us, as our ENTRYPOINT will be set with dotnet. Also the 1st build stage is thrown away once used so we cannot access the directory fully anyway.

Add the following to your Dockerfile which is our runtime stage:

FROM microsoft/dotnet:2.2-aspnetcore-runtime AS runtime-env
WORKDIR /publish
COPY --from=build-env /publish .
ENTRYPOINT ["dotnet","BookStoreApp.WebApi.dll"]

Rebuild the Image first:

docker build -t test-ep5 .

Then we can run the Image as a container process, overriding some of the ASP.NET Core environment variables using -e. We would override these usually when using docker-compose.

docker run -e "ASPNETCORE_ENVIRONMENT=Development" -e "ASPNETCORE_URLS=http://+:5003" -p 5003:5003 --rm -it test-ep5

You can then use your application of choice (I’m using Postman, more to come on that) to hit http://localhost:5003/api/bookstore. You should receive the following JSON payload:

[
    {
        "id": "5c6947093497a200016c0dee",
        "name": "Waterstones",
        "addressLine1": "The Dolphin & Anchor",
        "addressLine2": "West Street",
        "addressLine3": null,
        "city": "Chichester",
        "postCode": "PO19 1QD",
        "telephoneNumber": "01234 773030"
    }
]

And thats it, we are slowly starting to move our development and unit test pipeline into docker itself.

Next time

That’s it for today. Remember all code is on Github if you want it.

On our next episode we will start integrating MongoDB into the application.

Top comments (0)