Microservices Async Communication Using Ocelot Gateway, RabbitMQ, Docker, And Angular 14

We are going to discuss microservice asynchronous communication using the Ocelot API gateway and RabbitMQ Message Queue and containerization of the client application

I request you to read the following blogs for a better understanding of API Gateway and Microservice

Agenda

  • Implementation of Ocelot API Gateway
  • Implementation of Client Application to consume one service
  • Containerization using Docker

Prerequisites

  • Visual Studio 2022
  • .NET Core 6 SDK
  • Angular 14
  • Node Js
  • VS Code
  • SQL Server
  • Docker Desktop

Create Backend Product Owner and User Service as shown in this article

Update the controllers

ProductController

using Microsoft.AspNetCore.Mvc;
using ProductOwner.Microservice.Model;
using ProductOwner.Microservice.Services;

namespace ProductOwner.Microservice.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly IProductService productService;

        public ProductsController(IProductService _productService)
        {
            productService = _productService;
        }

        [HttpGet]
        public Task<IEnumerable<ProductDetails>> ProductListAsync()
        {
            var productList = productService.GetProductListAsync();
            return productList;

        }
        [HttpGet("{id}")]
        public Task<ProductDetails> GetProductByIdAsync(int Id)
        {
            return productService.GetProductByIdAsync(Id);
        }

        [HttpPost]
        public Task<ProductDetails> AddProductAsync(ProductDetails product)
        {
            var productData = productService.AddProductAsync(product);
            return productData;
        }

        [HttpPost("sendoffer")]
        public bool SendProductOfferAsync(ProductOfferDetail productOfferDetails)
        {
            bool isSent = false;
            if (productOfferDetails != null)
            {
                isSent = productService.SendProductOffer(productOfferDetails);

                return isSent;
            }
            return isSent;
        }
    }
}

UserOffersController

using Microsoft.AspNetCore.Mvc;
using ProductUser.Microservice.Model;
using ProductUser.Microservice.Services;
namespace ProductUser.Microservice.Controllers {
    [Route("api/[controller]")]
    [ApiController]
    public class UserOffersController: ControllerBase {
        private readonly IUserService userService;
        public UserOffersController(IUserService _userService) {
                userService = _userService;
            }
            [HttpGet]
        public Task < IEnumerable < ProductOfferDetail >> ProductListAsync() {
                var productList = userService.GetProductListAsync();
                return productList;
            }
            [HttpGet("{id}")]
        public Task < ProductOfferDetail > GetProductByIdAsync(int Id) {
            return userService.GetProductByIdAsync(Id);
        }
    }
}

Implementation of Ocelot API Gateway

Step 1

Create Ocelot API Gateway Project inside the same solution

Step 2

Configure your project

Step 3

Provide additional information about your project

Step 4

Install the following NuGet Package

Step 5

Configure a few services inside the Program class related to ocelot and CORS Policy

using Ocelot.DependencyInjection;
using Ocelot.Middleware;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddCors(options => {
    options.AddPolicy("CORSPolicy", builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
builder.Services.AddOcelot(builder.Configuration);
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseSwagger();
app.UseSwaggerUI();
app.UseCors("CORSPolicy");
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
await app.UseOcelot();
app.Run();

Step 6

Create an ocelot.json file and put the API’s endpoints and configuration related to that

{
  "GlobalConfiguration": {
    "BaseUrl": "https://localhost:7106/"
  },
  "Routes": [
    {
      "UpstreamPathTemplate": "/gateway/product",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/api/products",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 4201
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/gateway/product/{id}",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/api/products/{id}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 4201
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/gateway/product",
      "UpstreamHttpMethod": [ "Post" ],
      "DownstreamPathTemplate": "/api/products",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 4201
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/gateway/product/sendoffer",
      "UpstreamHttpMethod": [ "Post" ],
      "DownstreamPathTemplate": "/api/products/sendoffer",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 4201
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/gateway/offers",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/api/useroffers",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 4202
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/gateway/offers/{id}",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/api/useroffers/{id}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 4202
        }
      ]
    }
  ]
}
  • The Ocelot file has two sections: one is Global Configuration, which acts as an entry point of our application; and the other is Routes, which is used to define routes of our microservices.
  • UpstreamPathTemplate is used to receive client requests and redirects them to the particular microservice.
  • UpstreamHttpMethod is used to define HTTP attributes, which helps the gateway to get the type of request.
  • DownstreamTemplatePath is the microservice endpoint that takes the request from UpstreamPathTemplate.
  • DownstreamScheme defines the scheme of a request.
  • DownstreamHostAndPorts defines the hostname and port number of microservices that are present inside the lauchSetting.json file.

Implementation of Client Application to consume one service

Here we consume one microservice for demo purpose

Step 1

Create Angular Application

ng new ClientApplication

Step 2

We use bootstrap in this application. So, use the following command to install bootstrap.

npm install bootstrap

Step 3

Next, add the bootstrap script inside the angular.json file inside the scripts and styles section.

"styles": [
             "src/styles.css",
             "./node_modules/bootstrap/dist/css/bootstrap.min.css"
           ],
           "scripts": [
             "./node_modules/bootstrap/dist/js/bootstrap.min.js"
           ]

Step 4

Create ProductOfferDetail class inside the Model folder.

export class ProductOfferDetail {
    id?: number;
    productID?: number;
    productName?: string;
    productOfferDetails?: string;
}

Step 5

Create the following component

ng g c user

user.component.ts

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { ProductOfferDetail } from '../Model/ProductOfferDetail';
import { UserService } from '../user.service';

@Component({
    selector: 'app-user',
    templateUrl: './user.component.html',
    styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {
    ProductList ? : Observable < ProductOfferDetail[] > ;
    ProductList1 ? : Observable < ProductOfferDetail[] > ;
    constructor(private userService: UserService) {}
    ngOnInit() {
        setTimeout(() => {
            this.getUserList();
        }, 1000);
    }
    getUserList() {
        this.ProductList1 = this.userService.getUserList();
        this.ProductList = this.ProductList1;
    }
}

user.component.html

<nav class="navbar navbar-expand-lg navbar navbar-dark bg-dark">
    <a class="navbar-brand" href="#">Users Application</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarText">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item active">
          <a class="nav-link" routerLink="/">Home</a>
        </li>
        <li class="nav-item active">
          <a class="nav-link" routerLink="/users">Products</a>
        </li>
      </ul>
    </div>
  </nav>

  <form class="form-horizontal">
      <h1 style="text-align: center;">Microservices Product Application with .NET 6 Web API and Angular 14 </h1>
      <div>
        <div class="alert alert-success" style="text-align: center;"><b>Product List</b></div>
        <div class="table-responsive" style="text-align: center;">
          <table class="table table-striped">
            <thead>
            <tr>
              <th scope="col">#</th>
              <th scope="col">Product ID</th>
              <th scope="col">Product Name</th>
              <th scope="col">Product Offer</th>
            </tr>
          </thead>
          <tbody>
            <tr *ngFor="let product of ProductList | async; index as i">
              <th scope="row">{{ i + 1 }}</th>
              <td>{{product.productID}}</td>
              <td>{{product.productName}}</td>
              <td>{{product.productOfferDetails}}</td>
            </tr>
          </tbody>
          </table>
        </div>
      </div>
    </form>

Homepage Component

ng g c homepage

homepage.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-homepage',
  templateUrl: './homepage.component.html',
  styleUrls: ['./homepage.component.css']
})
export class HomepageComponent {
  title: string ="Ocelot Gateway Microservices Demo";
}

homepage.component.html

<nav class="navbar navbar-expand-lg navbar navbar-dark bg-dark">
    <a class="navbar-brand" href="#">{{title}}</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarText">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item">
            <a class="nav-link" routerLink="/users">Products</a>
          </li>
      </ul>
    </div>
  </nav>
  <h1>Welcome To Programming World!</h1>

Step 6

Next, Create a user service

ng g s user

user.service.ts

import { Injectable } from '@angular/core';
import configurl from '../assets/config/config.json';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { ProductOfferDetail } from './Model/ProductOfferDetail';

@Injectable({
    providedIn: 'root'
})
export class UserService {
    config = {
        ApiUrl: configurl.apiServer.url,
    };
    constructor(private http: HttpClient) {
        this.getJSON().subscribe((data) => {
            this.config.ApiUrl = data.apiServer.url;
        });
    }
    getUserList(): Observable < ProductOfferDetail[] > {
        return this.http.get < ProductOfferDetail[] > (this.config.ApiUrl + '/offers');
    }
    getUserDetailsById(id: string): Observable < ProductOfferDetail > {
        return this.http.get < ProductOfferDetail > (this.config.ApiUrl + '/offers' + id);
    }
    public getJSON(): Observable < any > {
        return this.http.get('./assets/config/config.json');
    }
}

Step 7

Define routes in an app routing. module file

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomepageComponent } from './homepage/homepage.component';
import { UserComponent } from './user/user.component';

const routes: Routes = [
  { path: '', component: HomepageComponent },
  { path: 'users', component: UserComponent }

];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Step 8

Configure and define all modules in the app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { UserComponent } from './user/user.component';
import { HomepageComponent } from './homepage/homepage.component';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent,
    UserComponent,
    HomepageComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Step 9

Create a config folder inside the assets folder, and then create a config.json file to define the backend server API URL.

{
    "apiServer": {
      "url": "https://localhost:7106/gateway",
      "version": "v1"
    }
}

Step 10

App Component:

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'ProductWebAPP';
}

app.component.html

<router-outlet></router-outlet>

Step 11

Create appEntryPoint.sh file which is used to set the dynamic port configuration

#!/bin/bash
envsubst < /usr/share/nginx/html/assets/config/config.template.json > /usr/share/nginx/html/assets/config/config.json && exec nginx -g 'daemon off;'

Step 12

Next, create nginx-custom.conf file which set some default Nginx server configuration

server {
    listen 80;
    location / {
        root / usr / share / nginx / html;
        index index.html index.htm;
        try_files $uri $uri / /index.html =404;
    }
}

Step 13

Create Dockerfile inside the root directory

### STAGE 1: Build ###
FROM node:16.10-alpine AS build
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build
### STAGE 2: Run ###
FROM nginx:1.17.1-alpine
COPY /nginx-custom.conf /etc/nginx/conf.d/default.conf
COPY --from=build /usr/src/app/dist/client-application /usr/share/nginx/html

# Copy the EntryPoint
COPY ./appEntryPoint.sh /
RUN chmod +x appEntryPoint.sh
ENTRYPOINT ["sh","/appEntryPoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

So, here you can see,

  • First, we take the node image for our application and set the docker directory, and then copy the package.json file and the remaining data inside the docker directory
  • In the second section, we take the Nginx server image and set the default configuration to that
  • Lastly, we set the entry point of our application and file which use to set the dynamic port at runtime.

Step 14

Also, Create config.template.json files inside the assets folder and config directory which use to set the backend API URL

{
    "apiServer": {
        "url": "${API_LINK}",
        "version": "v1"
    }
}

Step 15

Build Angular application

ng build --configuration production

Step 16

Create a Docker Compose file inside the root folder where our all projects are present

version: '3.5'
services:
  ProductOwner.Microservice:
   image: ${DOCKER_REGISTRY-}ownerservice:v1
   build:
    context: ./ProductOwner.Microservice
    dockerfile: Dockerfile
   environment: 
    - ASPNETCORE_ENVIRONMENT=Production
    - CONNECTIONSTRINGS__DEFAULTCONNECTION=Data Source=192.168.2.1,1433;Initial Catalog=ProductOwnerServiceDB;User Id=sa;Password=database
    - RABBIT_MQ_SERVER=192.168.2.1
    - RABBIT_MQ_USERNAME=guest
    - RABBIT_MQ_PASSWORD=guest
    - RABBITMQSETTINGS__EXCHANGENAME=OfferExchange
    - RABBITMQSETTINGS__EXCHHANGETYPE=direct
    - RABBITMQSETTINGS__QUEUENAME=offer_queue
    - RABBITMQSETTINGS__ROUTEKEY=offer_route
   ports:
    - "4201:80"
  ProductUser.Microservice:
   image: ${DOCKER_REGISTRY-}userservice:v1
   build:
    context: ./ProductUser.Microservice
    dockerfile: Dockerfile
   environment:
    - ASPNETCORE_ENVIRONMENT=Production   
    - CONNECTIONSTRINGS__DEFAULTCONNECTION=Data Source=192.168.224.1,1433;Initial Catalog=ProductUserServiceDB;User Id=sa;Password=database
    - RABBIT_MQ_SERVER=192.168.2.1
    - RABBIT_MQ_USERNAME=guest
    - RABBIT_MQ_PASSWORD=guest
    - RABBITMQSETTINGS__EXCHANGENAME=OfferExchange
    - RABBITMQSETTINGS__EXCHHANGETYPE=direct
    - RABBITMQSETTINGS__QUEUENAME=offer_queue
    - RABBITMQSETTINGS__ROUTEKEY=offer_route
   ports:
    - "4202:80"
  Client.Application:
   image: ${DOCKER_REGISTRY-}clientapplication:v1
   build:
    context: ./ClientApplication
    dockerfile: Dockerfile
   environment:
   - API_LINK=https://localhost:7106/gateway
   ports:
   - "4203:80"
  
  • Here you can see that we used the environment section to override the connection string that is present in the appsetting.json file and some environmental variables
  • Also, we put our machine IP Address over there in the connection string and port on which our SQL Server is running mode. Because if you put the server’s name, it will show some error while you are navigating your application which is run in a docker container.
  • You can get your IP address using the ipconfig command through CMD.

Step 17

Create Docker Image,

docker-compose build

docker-compose up

Step 18

Open docker desktop and inside that, you can see the images which we created

Step 19

In the container section, you can see images running and with the individual port number

Step 20

Product Owner Service

http://localhost:4201/swagger/index.html

Send Product Offer

Here we send the product offer and it will be stored inside RabbitMQ after that user consumer service will consume and save inside the database and when you run the client application you can see offer detail with the product

Step 21

Product User Service

http://localhost:4202/swagger/index.html

Step 22

Run Ocelot Gateway Project using Visual Studio

Step 23

Client Application

http://localhost:4203/

Conclusion

Here we discussed containerization and asynchronous communication between microservices using Ocelot API Gateway and RabbitMQ and step-by-step implementation

Happy Learning!