Implementation And Containerization Of Microservices Using .NET Core 6 And Docker

In this article, we will create two microservices using .NET Core 6 Web API and Containerization that services using Docker and connect them using Ocelot API Gateway.

Agenda

  • Introduction of Microservice
  • Introduction of Ocelot API Gateway
  • Step-by-step implementation of microservices
  • Containerization of Product and User Service using Docker
  • Implementation of Ocelot API Gateway

Prerequisites:

  • Understanding of .NET Core API, C#, and Docker
  • Visual Studio 2022
  • .NET Core 6 SDK
  • Docker Desktop
  • Postman

Introduction of Microservice

There are two types of architecture used for design applications: the first is monolithic, and the second one is microservices.

Monolithic Architecture

  • In a monolithic Application, all components are tightly coupled to one another and run using a single process.
  • It’s difficult to make changes in the future when monolithic architecture is used, because the components of the application will be tightly coupled to each other. This may impact all applications when trying to modify or introduce new things into the existing component.
  • Suppose we have an E-commerce application, and in that application, we used multiple components, such as Login, Posts, and News Feed. In that case, when we introduce or change something in one of the components, it will impact the application because the application is tightly coupled and interconnected to each other using a single service.
  • If we have deployed this type of application on the cloud, and in the future, we need to add something into the Posts Component, then we will be required to stop the whole application. This will impact performance and delivery services' efficiency. Also, it’s difficult to manage.

Microservice Architecture

  • In the Microservice Architecture, we create application components that will work independently, without being tightly coupled to each other, in their own environments.
  • Also, we can easily communicate with each other with the help of API Gateways and other such programs.
  • So, basically, when we use microservices, they will run their own environment, and they will not impact one another when we change something in one of the services.
  • Also, this is easy to manage while the application is in the development phase. Each developer will work independently on each service, and because of that, it is easy to manage and integrate components.

Introduction of Ocelot API Gateway

  • Ocelot is the API Gateway for the .NET Platform and which is work as the entry point of our application when we use microservice architecture.
  • The following diagram is from Microsoft Technical Documentation.

  • API gateway is present between the client and microservices and acts as middleware for communication between different microservices as shown in the above diagram.

Step-by-step implementation of microservices

Let’s start.

Step 1

Create a Blank Empty solution using a visual studio.

Step 2

Configure your project.

Step 3

Project Structure:

Step 4

Create a new folder inside the solution and name it Microservices. Next, create ProductMicroservice and UserMicroservice projects inside that.

Step 5

Configure the ProductMicroservice project and path of service.

Step 6

Provide additional information related to the project.

Step 7

Next, create the UserMicroservice project inside the Microservices folder and follow the same steps as we used to create ProductMicroservice.

Step 8

Create an Ocelot API Gateway web API project outside the Microservices folder.

Let’s start the implementation of ProductMicroservice

Project Structure

Step 1

 Install the following NuGet packages.

Step 2

Create Product Class inside the Model folder.

namespace ProductMicroservice.Model
{
    public class Product
    {
        public int ProductId { get; set; }
        public string ProductName { get; set; }
        public string ProductDescription { get; set; }
        public int ProductPrice { get; set; }
        public int ProductStock { get; set; }
    }
}

Step 3

Next, Create DbContextClass inside the Data folder.

using Microsoft.EntityFrameworkCore;
using ProductMicroservice.Model;

namespace ProductMicroservice.Data
{
    public class DbContextClass : DbContext
    {
        protected readonly IConfiguration Configuration;

        public DbContextClass(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
        }

        public DbSet<Product> Products { get; set; }
    }
}

Step 4

After that, create IProductService and ProductService inside the services folder.

IProductService

using ProductMicroservice.Model;

namespace ProductMicroservice.Services.Interface
{
    public interface IProductService
    {
        public IEnumerable<Product> GetProductList();
        public Product GetProductById(int id);
        public Product AddProduct(Product product);
        public Product UpdateProduct(Product product);
        public bool DeleteProduct(int Id);
    }
}

ProductService

using ProductMicroservice.Data;
using ProductMicroservice.Model;
using ProductMicroservice.Services.Interface;

namespace ProductMicroservice.Services
{
    public class ProductService : IProductService
    {
        private readonly DbContextClass _dbContext;

        public ProductService(DbContextClass dbContext)
        {
            _dbContext = dbContext;
        }
        public IEnumerable<Product> GetProductList()
        {
            return _dbContext.Products.ToList();
        }
        public Product GetProductById(int id)
        {
            return _dbContext.Products.Where(x => x.ProductId == id).FirstOrDefault();
        }

        public Product AddProduct(Product product)
        {
            var result = _dbContext.Products.Add(product);
            _dbContext.SaveChanges();
            return result.Entity;
        }

        public Product UpdateProduct(Product product)
        {
            var result = _dbContext.Products.Update(product);
            _dbContext.SaveChanges();
            return result.Entity;
        }
        public bool DeleteProduct(int Id)
        {
            var filteredData = _dbContext.Products.Where(x => x.ProductId == Id).FirstOrDefault();
            var result = _dbContext.Remove(filteredData);
            _dbContext.SaveChanges();
            return result != null ? true : false;
        }
    }
}

Step 5

Configure the connection string inside the appsetting.json file.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=DESKTOP;Initial Catalog=ProductServiceDB;User Id=sa;Password=****@1;"
  }
}

Step 6

Later on, configure a few services inside the Program.cs class.

using ProductMicroservice.Data;
using ProductMicroservice.Services;
using ProductMicroservice.Services.Interface;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddScoped < IProductService, ProductService > ();
builder.Services.AddDbContext < DbContextClass > ();
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.
//if (app.Environment.IsDevelopment())
//{
app.UseSwagger();
app.UseSwaggerUI();
//}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Step 7

Next, Create a ProductsController.cs.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ProductMicroservice.Model;
using ProductMicroservice.Services.Interface;

namespace ProductMicroservice.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly IProductService productService;
        public ProductsController(IProductService _productService)
        {
            productService = _productService;
        }
        [HttpGet]
        public IEnumerable<Product> ProductList()
        {
            var productList = productService.GetProductList();
            return productList;

        }
        [HttpGet("{id}")]
        public Product GetProductById(int id)
        {
            return productService.GetProductById(id);
        }
        [HttpPost]
        public Product AddProduct(Product product)
        {
            return productService.AddProduct(product);
        }
        [HttpPut]
        public Product UpdateProduct(Product product)
        {
            return productService.UpdateProduct(product);
        }
        [HttpDelete("{id}")]
        public bool DeleteProduct(int id)
        {
            return productService.DeleteProduct(id);
        }
    }
}

Step 8

Finally, execute the following command in the package manager console to create migration and create the database.

add-migration "Initial"
update-database

Implementation of UserMicroservice service

Project Structure

Step 1

 Install the following NuGet packages:

Step 2

Create a User Class inside the Model folder.

namespace UserMicroservice.Model
{
    public class User
    {
        public int UserId { get; set; }
        public string UserName { get; set; }
        public string Address { get; set; }
    }
}

Step 3

Next, Create DbContextClass inside the Data folder.

using Microsoft.EntityFrameworkCore;
using UserMicroservice.Model;

namespace UserMicroservice.Data
{
    public class DbContextClass : DbContext
    {
        protected readonly IConfiguration Configuration;

        public DbContextClass(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
        }

        public DbSet<User> Users { get; set; }
    }
}

Step 4

After that, create IUserService and UserService inside the services folder.

IUserService

using UserMicroservice.Model;

namespace UserMicroservice.Services.Interface
{
    public interface IUserService
    {
        public IEnumerable<User> GetUserList();
        public User GetUserById(int id);
        public User AddUser(User product);
        public User UpdateUser(User product);
        public bool DeleteUser(int Id);
    }
}

UserService

using UserMicroservice.Data;
using UserMicroservice.Model;
using UserMicroservice.Services.Interface;

namespace UserMicroservice.Services
{
    public class UserService : IUserService
    {
        private readonly DbContextClass _dbContext;

        public UserService(DbContextClass dbContext)
        {
            _dbContext = dbContext;
        }

        public User AddUser(User product)
        {
            var result = _dbContext.Users.Add(product);
            _dbContext.SaveChanges();
            return result.Entity;
        }

        public bool DeleteUser(int Id)
        {
            var filteredData = _dbContext.Users.Where(x => x.UserId == Id).FirstOrDefault();
            var result = _dbContext.Remove(filteredData);
            _dbContext.SaveChanges();
            return result != null ? true : false;
        }

        public User GetUserById(int id)
        {
            return _dbContext.Users.Where(x => x.UserId == id).FirstOrDefault();
        }

        public IEnumerable<User> GetUserList()
        {
            return _dbContext.Users.ToList();
        }

        public User UpdateUser(User product)
        {
            var result = _dbContext.Users.Update(product);
            _dbContext.SaveChanges();
            return result.Entity;
        }
    }
}

Step 5

Configure the connection string inside the appsetting.json file.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=DESKTOP;Initial Catalog=UserServiceDB;User Id=sa;Password=****@1;"
  }
}

Step 6

Later on, configure a few services inside Program.cs class.

using UserMicroservice.Data;
using UserMicroservice.Services;
using UserMicroservice.Services.Interface;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddScoped < IUserService, UserService > ();
builder.Services.AddDbContext < DbContextClass > ();
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.
//if (app.Environment.IsDevelopment())
//{
app.UseSwagger();
app.UseSwaggerUI();
//}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Step 7

Next, create a UsersController.cs.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using UserMicroservice.Model;
using UserMicroservice.Services.Interface;

namespace UserMicroservice.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UsersController : ControllerBase
    {
        private readonly IUserService userService;
        public UsersController(IUserService _userService)
        {
            userService = _userService;
        }

        [HttpGet]
        public IEnumerable<User> UserList()
        {
            var userList = userService.GetUserList();
            return userList;
        }

        [HttpGet("{id}")]
        public User GetUserById(int id)
        {
            return userService.GetUserById(id);
        }

        [HttpPost]
        public User AddUser(User user)
        {
            return userService.AddUser(user);
        }

        [HttpPut]
        public User UpdateUser(User user)
        {
            return userService.UpdateUser(user);
        }

        [HttpDelete("{id}")]
        public bool DeleteUser(int id)
        {
            return userService.DeleteUser(id);
        }
    }
}

Step 8

Finally, execute the following command in the package manager console to create a migration, and create the database.

add-migration "Initial"
update-database

Containerization of Product and User Service using Docker

Step 1

Create a Docker file for ProductMicroservice inside the root folder.

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /app

EXPOSE 443
EXPOSE 80 

# copy project csproj file and restore it in docker directory
COPY ./*.csproj ./
RUN dotnet restore

# Copy everything into the docker directory and build
COPY . .
RUN dotnet publish -c Release -o out

# Build runtime final image
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build /app/out .
ENTRYPOINT ["dotnet", "ProductMicroservice.dll"]

Step 2

Next, create a Docker file for UserMicroservice inside the root folder.

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /app

EXPOSE 443
EXPOSE 80 

# copy project csproj file and restore it in docker directory
COPY ./*.csproj ./
RUN dotnet restore

# Copy everything into the docker directory and build
COPY . .
RUN dotnet publish -c Release -o out

# Build runtime final image
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build /app/out .
ENTRYPOINT ["dotnet", "UserMicroservice.dll"]

Step 3

Create a docker-compose.yml file inside the main solution folder.

version: '3.5'
services:
  ProductService:
   image: ${DOCKER_REGISTRY-}producstmicroservice:v1
   build:
    context: ./ProductService
    dockerfile: Dockerfile
   environment: 
    - CONNECTIONSTRINGS__DEFAULTCONNECTION=Data Source=192.168.3.6,1433;Initial Catalog=ProductServiceDB;User Id=*****;Password=******
   ports:
    - "4201:80"
  UserService:
   image: ${DOCKER_REGISTRY-}usersmicroservice:v1
   build:
    context: ./UserService
    dockerfile: Dockerfile
   environment: 
    - CONNECTIONSTRINGS__DEFAULTCONNECTION=Data Source=192.168.3.6,1433;Initial Catalog=UserServiceDB;User Id=******;Password=******
   ports:
    - "4202:80"
  • Here you can see that we used the environment section to override the connection string that is present in the appsetting.json file
  • 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 direct server 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 4

Open CMD inside the main solution folder where the docker-compose.yml file is present and execute the following command.

docker-compose build
docker-compose up

Step 5

Open Docker Desktop and you can see inside that there are two images created and is in running mode.

Step 6

Open the Container section and there you will see your two images running inside a container with their individual port number.

Step 7

Use the following URLs to check your microservices functionality which is running inside docker container.

Product Service

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

User Service

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

Implementation of Ocelot API Gateway

Step 1

Install the Ocelot NuGet Package.

Step 2

Project Structure:

Step 3

Create an ocelot.json file for routing.

{
  "GlobalConfiguration": {
    "BaseUrl": "https://localhost:7062"
  },
  "Routes": [
    {
      "UpstreamPathTemplate": "/gateway/product",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/api/products",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 4201
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/gateway/product/{id}",
      "UpstreamHttpMethod": [ "Get", "Delete" ],
      "DownstreamPathTemplate": "/api/products/{id}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 4201
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/gateway/product",
      "UpstreamHttpMethod": [ "Post", "Put" ],
      "DownstreamPathTemplate": "/api/products",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 4201
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/gateway/user",
      "UpstreamHttpMethod": [ "Get" ],
      "DownstreamPathTemplate": "/api/users",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 4202
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/gateway/user/{id}",
      "UpstreamHttpMethod": [ "Get", "Delete" ],
      "DownstreamPathTemplate": "/api/users/{id}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 4202
        }
      ]
    },
    {
      "UpstreamPathTemplate": "/gateway/user",
      "UpstreamHttpMethod": [ "Post", "Put" ],
      "DownstreamPathTemplate": "/api/users",
      "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; the other is Routes, which is used to define routes of our microservices.
  • Here we put the port of our services that are running inside the docker container.
  • UpstreamPathTemplate is used to receive client requests and redirects them to the particular microservice.
  • UpstreamHttpMethod is used to define HTTP attributes, and 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.

Step 4

Configure the ocelot.json file inside Program.cs class and register services related to that.

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.AllowAnyMethod().AllowAnyHeader().AllowCredentials().SetIsOriginAllowed((hosts) => true));
});
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.
if (app.Environment.IsDevelopment()) {
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseCors("CORSPolicy");
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
await app.UseOcelot();
app.Run();

Step 5

Right-click on the solution and open the property. Then, apply to start on API Gateway projects that we have created.

Step 6

After running your Ocelot API Gateway project, you will see this swagger UI:

Step 7

Open the Postman and check the functionality.

Conclusion

In this article, we discussed the working of microservice architecture and Ocelot API gateway using .NET Core 6 API and containerization of applications using Docker.

Happy coding!


Similar Articles