API Rest with .net, ionic angular and Docker

In today's tutorial we are going to create a small API REST with a small service that you can use to learn how to create an API REST in C#. We will also talk about how to dockerize your endpoint in a container and how to expose your api  for development purposes and learn how to use SWAGGER (that will come preinstalled in your CSharp environment) to do small tests.


This endpoint's example controller simply makes an HTTP request to google to translate a string from one language to another. We can ignore this part of the code for the tutorial.


You will find 3 main folders in this example

- Translator: Which contains the endpoint of our small but sneaky translator

- Frondend: Which contains an ionic app with an angular engine running the frontend

- Tests : Which uses junit and node to run integration tests.


To complete this tutorial, you will need to first:

- Download the .net SDK  (The latest version at the time of writing is 5.0) you can download from this link.

- Download Docker Desktop from here.

-The code repository for this tutorial can be found here. We will be following this code file by file so you can pick up wherever you feel like.


Note about api keys

This example makes requests with google. If you want to see all of it in action, you'll need a google console cloud api. They are free to use and test. However, to follow this tutorial you won't really be needing this.


Downloading and setting up

Once you have downloaded the repository, follow the readme.md instructions to install.  The first steps you'll need to do is to compile and download all of the associated packages to our project. 


Regarding HTTPS Dev Certs

If you wish to install HTTPS certificates for development, you onlyn eed to type in your cli (windows + mac):

dotnet dev-certs https --trust


Cuadro de diálogo de advertencia de seguridad


Click yes and you'll have your developer https installed.

CSHARP .NET ENDPOINT

We will now navigate into the Translator folder in our project to inspect how we are building our csharp endpoint.   Remember this endpoint is written in C# and uses the .NET library to create a web endpoint that we can access via http calls.


Lets check Startup.cs , This is the script that is called at the beginning , when the endpoint is launched.


We now show the two main functions that we will normally edit when creating any endpoint.

public void ConfigureServices(IServiceCollection services) { var apikey = this.Configuration["GoogleApiKey"]; //Enable CORS services.AddCors(options => { options.AddPolicy("FreeForAll", builder => { builder.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod(); }); options.AddPolicy("AnotherPolicy", builder => { builder.WithOrigins("http://www.contoso.com") .AllowAnyHeader() .AllowAnyMethod(); }); }); services.AddControllers(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Translator", Version = "v1" }); }); services.AddSingleton<GoogleTranslateService>( x => new GoogleTranslateService(apikey)); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseSwagger(); app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Translator v1")); } app.UseHttpsRedirection(); app.UseRouting(); app.UseCors("FreeForAll"); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }

You can see how we are enabling Cors so that the server will accept any petition from any endpoint. The Line services.addControllers() adds all of the controllers that we have defined for each endpoint  in the Controllers folder. (more on that later).

HOW .NET Works

The way .NET works is fairly straightforward. The .net framework takes care of creating an http/s running machine that will take http requests and send http responses. Each route is defined by a controller that takes care of the actions that route has. We store each controller inside the Controllers Folder

If we take a close look at our Controller (TranslationRequestService.cs) we can see how this is being done. We use annotations like [ApiController] to indicate this is the controller for an API endpoint. We also use [Route] to indicate what the route should be (in this case, since we have written [Route("api/[controller]")] , our url endpoint will be api/TranslationRequest . We could however write anything different, like [Route("foo/bar")]. This will only indicate to .NET framework what the endpoint should be.

 1 2 3 4 5 6 7 8 9101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
using System;using System.Collections.Generic;using System.Linq;using System.Net.Http;using System.Net.Mime;using System.Net;using Microsoft.AspNetCore.Http;using System.Threading.Tasks;using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.Logging;using Translator.Models;using Translator.Services;using System.Web.Http.Cors;namespace Translator.Controllers{ [ApiController] [EnableCors(origins: "*", headers: "*", methods: "*")] [Route("api/[controller]")] public class TranslationRequestController : ControllerBase { private GoogleTranslateService googleTranslate; public TranslationRequestController( GoogleTranslateService googleTranslateService) { this.googleTranslate = googleTranslateService; } [HttpPost] [Consumes(MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task<ActionResult<TranslationResponse>> Post(TranslationRequest translationRequest) { if (translationRequest != null) { try { var x = await this.googleTranslate.GetTranslation(translationRequest); return Ok(x); } catch(HttpRequestException e) { return BadRequest(e.Message); } } else { return BadRequest("Translation request badly formed. Check the request Schema in api documentation"); } } }}

In our example, we are also using an Injectable service. If you check the constructor on line 26, our controller requires a service that we inject in runtime. If you check our Startup.cs file, we can see on line 55 how we add a singleton service.

55 | services.AddSingleton<GoogleTranslateService>( x => new GoogleTranslateService(apikey));

This , together with line 50. ensures that we create a singleton service 

What is a singleton service?

A singleton service, is a service that uses only one instance of itself throughout the entire application. In line 50, the app is loading all of the services defined in the services folder (more on this later) and whenever a module or controller requires an instance of that service, .NET will create a new instance of that service and inject it into our controllers. However, this may not be the desired outcome, specially if we want to focus on event microservices, where our controllers or services must trigger when certain events of our process have taken place.


Singleton Instancing on the other hand creates one single copy of the service and then shares that same instance across all of the application.


If we are listening to event emitters, if we don't have a singleton instance of the service. Whenever our process emits an event (or an exception), other services or controllers may not be able to listen to what just happened.


Depending on what we need, we may or may not require the use of Singleton Services. However, I decided to include this just in case, as an additional and valuable piece of information no matter what programming language you use.

ASYNCRONOUS PROGRAMMING AND C#

Asyncronous programming is used when external requests from our function will take "a little longer than usual" and we don't want our thread or process to block all of the other processes from using the CPU. For instance, if we are requesting data from a database, obtaining this information will take time, since we need to make http requests to other services that may be in other machines or systems. In order for our code to work faster and cleaner, we use asyncronous programming.


When a function is asynchronous, we do not expect it to return a specific value (or void). Instead, by using async in front of one of our functions, we return a Task. This task will be executed when the asynchronous code finishes.

ENDPOINTS AND HOW TO SEND AND RECEIVE DATA

In order to send and receive data into our post endpoint, check line 33, the "post" method is requesting an object of type TranslationRequest. If we navigate to our models folder we can check how this translation request is set.

 1 2 3 4 5 6 7 8 91011121314
using System;namespace Translator.Models{ public class TranslationRequest { public DateTime date { get; set; } public string from { get; set; } public string to { get; set; } public string text { get; set; } }}

This is the raw structure that we will feed into our endpoint. We will feed it with an appropiate json and the Request will take place.


Navigate to your Cli and into your /translator project folder and run dotnet run

Now navigate to http://localhost:5001/swagger/index.html. 

WHAT IS SWAGGER?

Swagger is a nice tool to test your endpoints. It comes prebundled  with .NET projects and is an excellent way of making your endpoint testable and to check if it's working properly. Swagger will list all of your available endpoints and allow you to test them. It is also compliable with OpenAPI, so your teammates will have a better understanding of what your api is formed by and how to use it.

Testing our endpoint

Hit Try it Out and paste the following json object to the request body

{
  "date": "2021-08-23T11:15:48.537Z",
  "from": "ES",
  "to": "EN",
  "text": "Hola Mundo"
}

html testing

When it's done, the server should return a 400 error. This is happening because the google API that the project is using is outdated and revoked. If you want this to work (not really needed for today's tutorial) then you should create a google api key in google cloud console.

In a similar way, you can also make post requests to localhost:5001/api/TranslationRequest to obtain the exact same response.


The idea behind this tutorial is really not to make this work. But to simply inform you of how to build a good and ordered endpoint with C# , and how to keep things as organized as possible.

Regarding Exception Handlers

Exceptions are events that our system fires when something has gone wrong. We catch these exceptions to avoid having unhandled exception errors. Lets take a look at our GoogleTranslateService.


What this service is really doing, is making a request to the google API (Via HTTPClient) and returning an object of type TranslationResponse.


However, take a look of how we are handling the try catch statement.

 1 2 3 4 5 6 7 8 910111213141516171819202122232425262728293031323334353637383940
using System.Collections.Generic;using System.Linq;using System;using System.Threading.Tasks;using Translator.Models;using System.Net.Http;using System.Web;using System.Net.Http.Json;namespace Translator.Services{ public class GoogleTranslateService { static readonly HttpClient client = new HttpClient(); private string apikey; public GoogleTranslateService(string apikey) { this.apikey = apikey; } public async Task<TranslationResponse> GetTranslation(TranslationRequest translationRequest) { var response = new TranslationResponse(); try { var url = "https://www.googleapis.com/language/translate/v2?key="+this.apikey+"&source="+translationRequest.from+"&target="+translationRequest.to+"&q="+HttpUtility.UrlEncode(translationRequest.text); Console.WriteLine("[REQUEST] {0}",url); GoogleResponse responseBody = await client.GetFromJsonAsync<GoogleResponse>(url); response.translatedText = responseBody.data.translations[0].translatedText; } catch(HttpRequestException e) { Console.WriteLine("[GOOGLE SERVICE] Exception Caught!"); Console.WriteLine("[GOOGLE SERVICE] Message :{0} ",e.Message); throw; } return response; } }}

Many times our endpoints will be emitting Exceptions and we need to make sure that we catch them. In line 35, we are indicating the system to throw an exception. This means, to elevate the exception handling to whomever invoked this function (in our case, the TranslationController). 


Why are we doing this here?

Because we don't want our service to be independent. In our case, if an exception takes place, then its not for the service to decide what http response to send. This is the controller's job.


If anything goes wrong in our service, we throw our exception so the parent process can catch it and decide what to do with it. In our case, we want it to send an error 400 response. If we were using this service from a different place (like another service) by developing it this way , we make it reusable and more robust.

MICROSERVICES IN DOCKER

Docker allows us to create microservices easily by running small containers that contain small instances of virtual machines that will run each one of our services independently.


As we mentioned before, this project is using two microservices. The first one being this C# microservice we just talked about, and the other one being a frontend developed in angular and ionic (of which we will talk in another chapter).


Lets navigate to the top folder and open the docker-compose.yml file

 1 2 3 4 5 6 7 8 910111213141516171819
version: "3.8"services: backend: build: ./Translator ports: - "5001:5001" - "5000:5000" restart: always expose: - 5001 environment: ASPNETCORE_ENVIRONMENT: Development frontend: build: ./frontend restart: always ports: - "80:80" expose: - 80

USINg docker to DOCKERIZE OUR ENDPOINT

Docker uses docker-compose to set up multiple services. In this case, we are defining two services, backend and frontend. Each one is defined by a Dockerfile inside of each one of the build folders.


You can also use build images that are downloaded from the internet to set up machines quickly that will run your code. If you haven't toyed with docker, I strongly suggest you do, since it's becoming one of the fastest growing requirements for any developer.


Lets head to the dockerfile inside the Translator folder.

 1 2 3 4 5 6 7 8 91011121314151617
# syntax=docker/dockerfile:1FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build-envWORKDIR /app# Copy csproj and restore as distinct layersCOPY *.csproj ./RUN dotnet restore# Copy everything else and buildCOPY ./ ./RUN dotnet publish -o out# Build runtime imageFROM mcr.microsoft.com/dotnet/aspnet:5.0WORKDIR /appCOPY --from=build-env /app/out .ENTRYPOINT ["dotnet", "Translator.dll"]

A dockerfile is simply a script that tells Docker how the virtual machine should be. If you are not familiar, you can find a refference of how to use dockerfiles here.


What we are instructing docker to do in this file, is to 

  1.  Download the dotnet sdk official docker image
  2.  copy  any .csproj files into the ./ root folder of the dotnet image we just downloaded
  3.  run dotnet restore inside of the virtual machine so the image we are creating downloads all of the files it needs to operate
  4.  copy our project folder into the docker image
  5.  compile our project
  6.  Create a new aspnet image for runtime
  7.  change our working directory to /app
  8.  Copy our compilation defined in line 2 to our aspnet runtime image /out folder (where it works from).


Our final line of code defines the entrypoint the machine should use (what the initial command should be). In this case, it's running dotnet with the Translator.dll file generated by our system. (we could change this name to any other file we were generating.


We can either run docker run from our Translator folder, or we can run docker-compose up from our root folder.


If you run docker-compose up the system will fire both microservices. Lets head towards the dockerfile inside the frontend folder and see what the other file is doing.

 1 2 3 4 5 6 7 8 910
FROM node:13-alpine as buildWORKDIR /appCOPY ./package*.json /app/RUN npm install -g ionicRUN npm installCOPY ./ /app/RUN npm run-script buildFROM nginx:alpineRUN rm -rf /usr/share/nginx/html/*COPY --from=build /app/www/ /usr/share/nginx/html/

This dockerfile is doing something similar. We are first downloading the original node image as a build image. We are executing the commands needed for ionic framework + angular to work inside of it and creating a build of the project (not needed for this tutorial for now).


We then create another image (line 8 : FROM nginx:alpine) to store the javascript frontend website and then copying the /www folder where ionic compiled the javascript framework and pasting it into the nginx/html public folder that nginx uses to dispatch and work.


This is a very common setup for Docker. If you take a closer look, we are using docker to create containers that do stuff (in each case, two containers were created per microservice, one for building the system and another one to deploy the service).


This is how we manage microservices in Docker. We can then set up each service the way we want it and how we want it and docker will take care of routing each microservice to a network interface and make it work.


When you have run the microservices in docker, you can access each one of the microservices in the indicated endpoints by the docker-compose.yml

Summing up and extra homework.

I hope this tutorial was useful. Feel free to toy with it or use it however you wish. Our next update will be a tutorial on the frontend section of this project, explaining how we connected to our backend using an ionic framework application.


​Stay tuned and stay connected! Like share and subscribe.

Our reader's favorite picks

Email *
Suscribe