This repository contains the solution to the CalculatorService challenge. There are two components in the repository:
- CalculatorService.Server: Web API that will perform the required operatiosn
- CalculatorService.Client: Console application that will consume the API
Both components can be build with the dotnet build
command from the root directory.
Additionally, the Server can be build as a container image. To do so run the docker build command with context in the root directory and pointing to the server Dockerfile: docker build -t evicertichallengeserver -f .\Server\src\CalculatorService.Server\Dockerfile .
The Client executable will be found under the /bin
directory of the CalculatorService.Client project.
To generate all the required files to deploy the application run the dotnet publish -c Release
command.
The generated executable will launch a Kestrel server that should be run behind an externally-facing reverse proxy (such as Nginx or IIS in Windows)
The generated container image can also be used as a deployment artifact.
The application is just a process listening to a port, so to run it we just need to point the incoming requests to the application port. The port the application listens to can be configured easily in two ways:
- Setting the
Urls
node in appsettings.json as follows : { "Urls": "http://localhost:8080" } - Setting the environment variable
ASPNETCORE_URLS
(ASPNETCORE_URLS=http://localhost:8080/
)
The CalculatorService.Server contains the logic dealin whit the API itself, such the route mapping and the API configuration.
The CalculatorService.Server.Application contains the business loginc, organized by vertical slices. Inside the Calculator folder there are different folders for each of the opperations, so that all the logic related to an operation is close together. The Journal folder contains everything required to track all the operations related by a tracking Id.
To easily implement this vertical architecture the MediatR
package has been used, so that the CalculatorController only has a dependency on the mediator, and the mediator is the one concerned with using the correct handler for each request.
Using MediatR
has also allowed to implement the tracking of the operations as an aspect of the application by using a Behavior that applies to all endpoints which requets implement IOperationRequest
and which responses implement IOperationResponse
.
This allowed to completely decouple the operations logic from the tracking logic. And if a new operation is implemented, as long as its request and response types implement the mentioned interfaces, it will be automatically tracked.
All the requets and responses are loged to the console and to a file that will rotate daily and when reachign a size of 1GB. These requests will contain information about the environment and process and a RequestId
property that will allow relating all the log entries that were generated by the same API call.
The implementation of the IJournalService
, which deals with storing and retrieving the users's operations, has been simplified by using a ConcurrentDictionary that uses the tracking id as key and stores a collection of the operations. By doing so we have a simple and safe way to trak the operations. This dictionary has been registered in the DI container as a singleton so the data is persisted through the lifetime of the service.
In a more polished environment the use of a complete async approach, such as a message queue, is recommended for this operation. In this way storing the journal would be implemented in a fire-and-forget way, avoiding that a possible issue with the tracking impacts the operation itself.
To simplify the operations, all the requets deal with int
types. But by doing so we expose ourselves to int overflows, wich would return incorrect responses. To avoid it the cheked keyword has been used, prefering an exception to an incorrect response.