Building Microservices Service using ASP.NET Core 7.0, MongoDB, and Docker Container
This article is a continuation of my previous article, in this article we will implement the microservice concept for the catalog.API service using Asp.Net Web API, Docker, MongoDB, and Swagger.
Introduction
To build microservices using ASP.NET Core and MongoDB, and package them into Docker containers, you can follow these steps:
- Install Docker: Install Docker on your machine, which will allow you to create and manage containers.
- Create an ASP.NET Core application: Create an ASP.NET Core application using Visual Studio or the .NET Core CLI.
- Add MongoDB support: Add MongoDB support to the application by installing MongoDB.Driver NuGet package and configure the database connection.
- Containerize the application: Create a Dockerfile that specifies the application’s dependencies and settings, and build a Docker image. You can also use a Docker Compose file to define the services and their dependencies.
- Build and run the container: Build the Docker image and run a container from it. You can use the Docker CLI or a graphical user interface like Docker Desktop.
- Scale the application: You can use Docker Swarm or Kubernetes to manage multiple instances of the application, which will improve performance and availability.
- Monitor and manage the application: Use tools like Docker Compose, Kubernetes Dashboard, and Prometheus to monitor and manage the application’s performance, logs, and health.
- These steps will allow you to build and deploy microservices using ASP.NET Core, MongoDB, and Docker containers.
In the final Test microservice using swagger :
Analysis & Design
This project will be the REST APIs which basically perform CRUD operations on Catalog databases.
Our main use cases:
- Listing Products and Categories
- Get Product with product Id
- Get Products with a category
- Create new Product
- Update Product
- Delete Product
Starting Our Project
Install Docker
To install Docker on your machine, you can follow these general steps:
- Choose the right version: Choose the appropriate Docker version for your operating system. Docker provides versions for Windows, macOS, and various Linux distributions.
- Download Docker: Download the Docker installation package from the official Docker website. The website will automatically detect your operating system and provide you with the appropriate download link.
- Install Docker: Once you have downloaded the installation package, run the installer and follow the instructions to complete the installation process.
- Verify the installation: After the installation is complete, open a terminal or command prompt and type docker — version to verify that Docker is installed correctly. You should see the version number of Docker displayed in the output.
- Start Docker: Start the Docker daemon by running the docker command. On Windows and macOS, Docker should start automatically when you log in to your computer.
These steps are general and can vary depending on your operating system and version of Docker. You can refer to the Docker documentation for detailed instructions on how to install Docker on your specific platform.
Setup Mongo Database
To set up a MongoDB database in Docker, you can follow these general steps:
- Pull the MongoDB image: Open a terminal or command prompt and run the following command to pull the MongoDB image from Docker Hub:
- Start a MongoDB container: Run the following command to start a new container based on the MongoDB image:
This command will start a new container named
aspnetrun-mongo and map the container’s port 27017 to the
host’s port 27017.
- Verify the container is running: Run the following command to verify that the container is running:
This command will display a list of running containers, including
the aspnetrun-mongo container.
Install Visual Studio Code
To install Visual Studio Code, you can follow these steps:
- Go to the Visual Studio Code website: Open your web browser and navigate to the Visual Studio Code website at https://code.visualstudio.com/.
- Download the installer: Click on the “Download for [your operating system]” button on the homepage. This will download the installer package for your operating system.
- Run the installer: Once the download is complete, locate the installer package in your Downloads folder or your browser’s download history, and run the installer.
- Follow the installation prompts: Follow the installation prompts to install Visual Studio Code on your machine. You can accept the default options, or customize the installation to your preferences.
- Launch Visual Studio Code: Once the installation is complete, launch Visual Studio Code from the Start menu on Windows, the Applications folder on macOS, or the Applications launcher on Linux.
- Install extensions: Visual Studio Code supports a wide range of extensions that can enhance your development experience. You can install extensions from the Visual Studio Code Marketplace by clicking on the “Extensions” icon on the left-hand side of the screen and searching for the extensions you want.
Create an ASP.NET Core application
- Open Terminal Create folder aspnetcore-microservices=> src => services => catalog
- Create asp net core web api in terminal using “dotnet new webapi -n catalog.api”
- Open project in visual studio code
In explore we can see as below:
Library & Frameworks
For Catalog microservices, we have two libraries in our Nuget Packages,
- Mongo.DB.Driver — To connect mongo database
“dotnet add package MongoDB.Driver”
- Swashbuckle.AspNetCore — To generate a swagger index page
“dotnet add package Swashbuckle.AspNetCore”
Create Entities
Create an Entities folder for your project. This will be the MongoDB collections of your project. In this section, we will use the MongoDB Client when connecting the database. That’s why we write the entity classes at first.
Add Folder Entities and Add New Class -> Product class and Copy the script bellow
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson;
namespace Catalog.Api.Entities
{
public class Product
{
[BsonId]
[BsonRepresentation(MongoDB.Bson.BsonType.ObjectId)]
public string Id { get; set; }
[BsonElement("Name")]
public string Name { get; set; }
public string Category { get; set; }
public string Summary { get; set; }
public string Description { get; set; }
public string ImageFile { get; set; }
public decimal Price { get; set; }
}
}
There are Bson annotations that provide to mark properties for the database mapping. I.e. BsonId is the primary key for Product collection.
Create Data Layer
Create a Data folder for your project. This will be the MongoDB collections of your project.
In order to manage these entities, we should create a data structure. To work with a database, we will use this class with the MongoDb Client. In order to wrap these classes we will create that provide data access over the Context classes.
To store these entities, we start with ICatalogContext interface.
Add Folder Data=> Interfaces and Add New Class -> ICatalogContext class and Copy the script bellow
using Catalog.Api.Entities;
using MongoDB.Driver;
namespace Catalog.Api.Data.Interfaces
{
public interface ICatalogContext
{
IMongoCollection<Product> Products { get; }
}
}
Basically, what we expect from our db context object is Product collections.
Continue with implementation and create CatalogContext class.
In Folder Data Add New Class => CatalogContext class and Copy the script bellow
using Catalog.Api.Data.Interfaces;
using Catalog.Api.Entities;
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
namespace Catalog.Api.Data
{
public class CatalogContext : ICatalogContext
{
public CatalogContext(IConfiguration configuration)
{
var client = new MongoClient(configuration.GetValue<string>("DatabaseSettings:ConnectionString"));
var database = client.GetDatabase(configuration.GetValue<string>("DatabaseSettings:DatabaseName"));
Products = database.GetCollection<Product>(configuration.GetValue<string>("DatabaseSettings:CollectionName"));
CatalogContextSeed.SeedData(Products);
}
public IMongoCollection<Product> Products { get; }
}
}using Catalog.Api.Data.Interfaces;
using Catalog.Api.Entities;
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
namespace Catalog.Api.Data
{
public class CatalogContext : ICatalogContext
{
public CatalogContext(IConfiguration configuration)
{
var client = new MongoClient(configuration.GetValue<string>("DatabaseSettings:ConnectionString"));
var database = client.GetDatabase(configuration.GetValue<string>("DatabaseSettings:DatabaseName"));
Products = database.GetCollection<Product>(configuration.GetValue<string>("DatabaseSettings:CollectionName"));
CatalogContextSeed.SeedData(Products);
}
public IMongoCollection<Product> Products { get; }
}
}
In this class, the constructor initiates MongoDB connection using the MongoClient library. And load the Products collection.
In Folder Data Add New Class => CatalogContextSeed class and Copy the script bellow
The code gets the connection string from settings. In Asp.Net Core this configuration stored appsettings.json file:
"DatabaseSettings": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "CatalogDb",
"CollectionName": "Products"
}
Register DataContext into ASP.NET Dependency Injection
we should register these repository classes into ASP.NET Built-in Dependency Injection engine. That means we should recognize these classes in asp.net core in order to use them from the frontend side in the web application.
Open Program.cs => add
using Catalog.Api.Data.Interfaces;
using Catalog.Api.Data;
using Catalog.Api.Entities;
And bellow
builder.Services.AddEndpointsApiExplorer();
add
builder.Services.AddSwaggerGen();
builder.Services.AddScoped<ICatalogContext, CatalogContext>();
Mongo DB context object should register in DI when starting the application. Ensure that DbContext object in ConfigureServices method is configured properly.
Create Business Layer
For the Business Logic Layer, we should create a new folder whose name could be the Service — Application — Manager — Repository in order to manage business operations using data access layer objects.
For the Business Logic Layer, we are using the Repository folder in order to manage business operations using data access layer objects.
According to our main use cases, we will create interface and implementation classes in our business layer.
- Listing Products and Categories
- Get Product with product Id
- Get Products with a category
- Create new Product
- Update Product
- Delete Product
Add Folder Repository=> Interfaces and Add New Class -> IProductRepository class and Copy the script bellow
using Catalog.Api.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Catalog.Api.Repositories.Interfaces
{
public interface IProductRepository
{
Task<IEnumerable<Product>> GetProducts();
Task<Product> GetProduct(string id);
Task<IEnumerable<Product>> GetProductByName(string name);
Task<IEnumerable<Product>> GetProductByCategory(string categoryName);
Task CreateProduct(Product product);
Task<bool> UpdateProduct(Product product);
Task<bool> DeleteProduct(string id);
}
}
Let’s implement these interfaces with using Data layer objects. In our case Data layer represents from Mongo Client library so we should use DBContext object. In order to use CatalogContext object which represent us DB Layer, the constructor should use dependency injection to inject the database context(CatalogContext) into the ProductRepository class.
In Folder Repository Add New Class -> ProductRepository class and Copy the script bellow
using Catalog.Api.Data.Interfaces;
using Catalog.Api.Entities;
using Catalog.Api.Repositories.Interfaces;
using MongoDB.Driver;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Catalog.Api.Repositories
{
public class ProductRepository : IProductRepository
{
private readonly ICatalogContext _context;
public ProductRepository(ICatalogContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task<IEnumerable<Product>> GetProducts()
{
return await _context
.Products
.Find(p => true)
.ToListAsync();
}
public async Task<Product> GetProduct(string id)
{
return await _context
.Products
.Find(p => p.Id == id)
.FirstOrDefaultAsync();
}
public async Task<IEnumerable<Product>> GetProductByName(string name)
{
FilterDefinition<Product> filter = Builders<Product>.Filter.ElemMatch(p => p.Name, name);
return await _context
.Products
.Find(filter)
.ToListAsync();
}
public async Task<IEnumerable<Product>> GetProductByCategory(string categoryName)
{
FilterDefinition<Product> filter = Builders<Product>.Filter.Eq(p => p.Category, categoryName);
return await _context
.Products
.Find(filter)
.ToListAsync();
}
public async Task CreateProduct(Product product)
{
await _context.Products.InsertOneAsync(product);
}
public async Task<bool> UpdateProduct(Product product)
{
var updateResult = await _context
.Products
.ReplaceOneAsync(filter: g => g.Id == product.Id, replacement: product);
return updateResult.IsAcknowledged
&& updateResult.ModifiedCount > 0;
}
public async Task<bool> DeleteProduct(string id)
{
FilterDefinition<Product> filter = Builders<Product>.Filter.Eq(p => p.Id, id);
DeleteResult deleteResult = await _context
.Products
.DeleteOneAsync(filter);
return deleteResult.IsAcknowledged
&& deleteResult.DeletedCount > 0;
}
}
}
Let’s implement these interfaces using Data layer objects. In our case Data layer represents from Mongo Client library so we should use DBContext object. In order to use the CatalogContext object which represents us DB Layer, the constructor should use dependency injection to inject the database context(CatalogContext) into the ProductRepository class.
Register Repository into ASP.NET Dependency Injection
We should register these repository classes into ASP.NET Built-in Dependency Injection engine. That means we should recognize these classes in asp.net core in order to use them from the frontend side in the web application.
Open Program.cs => add
builder.Services.AddScoped<IProductRepository, ProductRepository>();
bellow
builder.Services.AddScoped<ICatalogContext, CatalogContext>();
Create Presentation Layer
Since creating a Web API template for the ASP.NET Core project, the presentation layer will be Controller classes which produce the API layer.
Locate the Controller folder and create CatalogController class.
In Folder Controller add CatalogController class and copy the code below:
using Catalog.Api.Entities;
using Catalog.Api.Repositories.Interfaces;
using DnsClient.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
namespace Catalog.Api.Controllers
{
[ApiController]
[Route("api/v1/[controller]")]
public class CatalogController : ControllerBase
{
private readonly IProductRepository _repository;
private readonly ILogger<CatalogController> _logger;
public CatalogController(IProductRepository repository, ILogger<CatalogController> logger)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<Product>), (int)HttpStatusCode.OK)]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
var products = await _repository.GetProducts();
return Ok(products);
}
[HttpGet("{id:length(24)}", Name = "GetProduct")]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType(typeof(Product), (int)HttpStatusCode.OK)]
public async Task<ActionResult<Product>> GetProductById(string id)
{
var product = await _repository.GetProduct(id);
if (product == null)
{
_logger.LogError($"Product with id: {id}, not found.");
return NotFound();
}
return Ok(product);
}
[Route("[action]/{category}", Name = "GetProductByCategory")]
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<Product>), (int)HttpStatusCode.OK)]
public async Task<ActionResult<IEnumerable<Product>>> GetProductByCategory(string category)
{
var products = await _repository.GetProductByCategory(category);
return Ok(products);
}
[Route("[action]/{name}", Name = "GetProductByName")]
[HttpGet]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
[ProducesResponseType(typeof(IEnumerable<Product>), (int)HttpStatusCode.OK)]
public async Task<ActionResult<IEnumerable<Product>>> GetProductByName(string name)
{
var items = await _repository.GetProductByName(name);
if (items == null)
{
_logger.LogError($"Products with name: {name} not found.");
return NotFound();
}
return Ok(items);
}
[HttpPost]
[ProducesResponseType(typeof(Product), (int)HttpStatusCode.OK)]
public async Task<ActionResult<Product>> CreateProduct([FromBody] Product product)
{
await _repository.CreateProduct(product);
return CreatedAtRoute("GetProduct", new { id = product.Id }, product);
}
[HttpPut]
[ProducesResponseType(typeof(Product), (int)HttpStatusCode.OK)]
public async Task<IActionResult> UpdateProduct([FromBody] Product product)
{
return Ok(await _repository.UpdateProduct(product));
}
[HttpDelete("{id:length(24)}", Name = "DeleteProduct")]
[ProducesResponseType(typeof(Product), (int)HttpStatusCode.OK)]
public async Task<IActionResult> DeleteProductById(string id)
{
return Ok(await _repository.DeleteProduct(id));
}
}
}
Run Application
Now the Catalog microservice Web API application is ready to run.
In Terminal Type dotnet run
In browser type :
http://localhost:5029/swagger/index.html
Add Docker Compose and Dockerfile
We can add only Dockerfile to make dokerize the Web API application but we will integrate our API project with MongoDB docker image, so we should create a docker-compose file with the Dockerfile of the API project.
In Folder Services Add => ..Container Orchestration Support
Continue with default values.
Dockerfile and docker-compose files are created.
Add Docker-compose.yml is a command-line file used during development and testing, where necessary definitions are made for multi-container running applications.
In Docker-compose.yml copy
version: '3.4'
services:
catalogdb:
image: mongo
catalog.api:
image: ${DOCKER_REGISTRY-}catalogapi
build:
context: .
dockerfile: Src/Services/Catalog/Catalog.Api/Dockerfile
Add docker-compose.override.yml and copy
version: '3.4'
services:
catalogdb:
container_name: catalogdb
restart: always
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db
catalog.api:
container_name: catalog.api
environment:
- "DatabaseSettings:ConnectionString=mongodb://catalogdb:27017"
- "DatabaseSettings:DatabaseName=CatalogDb"
- "DatabaseSettings:CollectionName=Products"
- ASPNETCORE_ENVIRONMENT=Development
depends_on:
- catalogdb
ports:
- "8000:80"
In docker-compose.yml file, created 2 images 1 is for mongoDb whose name is catalogdb, and 2 is web API project which name is catalog.api.
After that, we configure these images into docker-compose.override.yml file.
In override file said that;
Catalogdb which is the mongo database will open the 27017 port.
Catalog.api which is our developed web API project depends on catalogdb and opens the port on 8000 and we override to connection string with catalogdb.
Run the below command on top of the project folder which includes docker-compose.yml files.
docker-compose -f docker-compose.yml -f docker-compose.override.yml up –build