Getting to the meat on the bones now. So what have we got so far?
- A Monolith WebApp that contains service endpoints within it to do maths on two input numbers from a user.
- A DockerFile we can use to Dockerise that App and store the Docker Image in a local library
- A set of instructions to create a self-hosted Docker Registry in case you don’t want to use Docker Hub, or a cloud hosted Docker Registry.
- An automated pipeline which does some simple Test Driven Development on the code inside the monolith using Azure DevOps
- A separate automated pipeline which on success of the previous pipeline, dockerises the app straight from source, and uploads it to our self-hosted repository.
Now to start to break up the monolith.
I created a new dotnet solution called multiservice, with four individual projects, “frontend”, “minusService”, “addService” and “multiplyService”.
dotnet new sln -o multiService
cd multiService
dotnet new mvc -o frontEnd
dotnet new webapi -o addService
dotnet new webapi -o minusService
dotnet new webapi -o multiplyService
dotnet sln multiService.sln add ./frontEnd/frontEnd.csproj
dotnet sln multiService.sln add ./addService/addService.csproj
dotnet sln multiService.sln add ./minusService/minusService.csproj
dotnet sln multiService.sln add ./multiplyService/multiplyService.csproj
This scaffolds up the skeleton of the multiService app. We’ll do a quick build at the solution level to check it all builds.
(from in the ./frontend project folder)
dotnet build
We can copy the code we wrote for the monolith website into frontend, do a quick layout change within frontEnd/Views/Shared/_Layout.cshtml to show we’re moving to a multi service architecture and try running it.
Amend the <title> and <footer> content to say MultiService instead of Monolithsvc.
dotnet run
So the app is running within a bigger solution that contains four separate projects.
Now we go back to our TDD principles and right some test code that we are about to write to seperate the first service which adds two numbers. We are seperating our concerns making the code easier to maintain in the long run.
Starting with the addService, lets create a TDD test project.
dotnet new mstest -o addService.Tests
…the unit test project currently has no knowledge of the addService project, so lets add in that as a reference (this will make changes to the addService.Tests.csproj file to link the two up)
dotnet add addService.Tests.csproj reference ../addService
I rename the UnitTest1.cs to unitTests_addController.cs and match up the class name to that for ease of support.
Now for some simple test code….
using Microsoft.VisualStudio.TestTools.UnitTesting;
using addService.Controllers;
namespace addService.Tests
{
[TestClass]
public class unitTests_addController
{
[TestMethod]
public void addControllerTwoPlusOneEqualsThree()
{
//instantiates an addController instance (ARRANGE)
var controller = new addController();
//we run the Get Method and pass it two values (ACT)
var result = controller.Get(2,1);
//we get the value of the result (because the result object above contains more than just the
//add result we're after)
var actualResult = result.Value;
//and then we check the result (ASSERT)
Assert.AreEqual(3, actualResult);
}
}
}
Save that, and run a dotnet test. It should bomb something like this:
In the TDD Cycle here, we’ve done step 1, created a test and made it fail. Now to fix it so that it goes green.
We’re now going to change the addService project and create the controller we were just trying to instantiate….
Go to MultiService / addService / Controllers and create a file called addController.cs (we should be able to cut and paste the code from the monolithsvc example of that file, we’re just moving the code into an isolated project so it can run outside of the main monolithsvc web app now on our route to Microservices.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Cors;
namespace addService.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class addController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<int> Get(int a, int b)
{
return (a + b);
}
}
}
..aah just noticed a problem, I ran dotnet test and got this:
This is basically saying addService.Tests project needs some dependencies we’ve forgotten about to reference to enable the mvc code to work outside of a browser. Check the addService.Tests.csproj files between monolithsvc and multiservice match.
Save and Run another dotnet test.
Woop! Woop! Excellent. We’ve got some controller code that makes the test pass. The last step of TDD dictates we now tidy up the code, refactor it, make it official. In this simple example I’m going to leave that be.
We now have a dev loop forming here, create a test that tests out the code you’re about to write, it fails, write the code, make it pass and refactor.
We write lots of small, fast running tests and if written correctly you as a developer are describing how your app should work. If someone else comes along and wants to make a change they can run the same tests you do and ensure that they don’t break any existing functionality. Awesome!
I’m going to leave it at one test here, but we can test all sorts of failure conditions, checking for boundary conditions, values being specified as integers….all sorts.
Since we’ve got two other services, rinse and repeat for multiply and for minus.
Once each of the projects is done, how are we going to test the whole app works. We can do that in one of two ways, either by using dotnet or by using docker.
With dotnet we could go into each of the service projects, and do a dotnet run and then point the frontend app to each of the URLs that dotnet run comes out with, bit clumsy.
If we use a combination of Docker and Docker Compose we can create images of our apps and then get them all to spin up at the same time. If you recall from the Docker tutorial DockerFiles define a single application image. How do we string them all together, if we’ve now got 4+ services.
# Sample contents of Dockerfile, we've got one of these in each service
# Stage 1
FROM microsoft/dotnet:2.1-sdk AS builder
WORKDIR /source
# caches restore result by copying csproj file separately
COPY *.csproj .
RUN dotnet restore
# copies the rest of your code
COPY . .
RUN dotnet publish --output /app/ --configuration Release
# Stage 2
FROM microsoft/dotnet:2.1-aspnetcore-runtime
WORKDIR /app
COPY --from=builder /app .
ENTRYPOINT ["dotnet", "multiplyService.dll"]
What we’ve got below is a docker-compose.yml file, it will build each of the services into images on your local machine, then if you run a docker-compose up it’ll try and run them all.
version: '3'
services:
addsvc:
build: './addService'
ports:
- "18081:80"
minussvc:
build: './minusService'
ports:
- "18082:80"
multiplysvc:
build: './multiplyService'
ports:
- "18083:80"
frontend:
build: './frontEnd'
ports:
- "18080:80"
Lets see that in action:
Now here, we can see each of the images didn’t need a build, they we’re cached so each of the services came up really quickly. If they weren’t in the local repository we would see a long list of build commands, but the end result is the same.
Now it looks as if all the services are listening on the host port 80, which wouldn’t be possible, but if we go back to the docker-compose.yml we can see that each service is mapped to a port in the 1808x range. So we should be able to hit the frontend on localhost:18080 and see our frontend working.
Nice! I accept I’ve skipped over a few applications bits there, but you should be able to see that move from monolith to Microservices architecture.
Here’s the code repo of the multiservice: https://github.com/r3adm3/multiservice