Microservice Implementation using ASP NET Core 6 — Part 1

Muhammad Khoirudin
15 min readFeb 1, 2023

--

Hi All, in previous article we have discussed about comparing microservice with other approach. Right know we will implement microservice using ASP NET Core 6. Before proceeding, let’s get definition of microservice. Microservice is architectural design to create an application that consists of various separate service units, but still connected between each other. Each service could be developed using different technology stack rely on needs.

In this part, I’m going to show you about separate domain model / domain class before determining how many service need to be developed and focus on it. All of service will be developed using ASP NET Core but having different database engine. The last topic in this part about creating API Gateway using ocelot. Check it out.

You can see complete code in my GitHub Repository

Study Case

We will develop simple order application for the restaurant according to my perspective. Domain model that will we have are the following,

  1. Food Category (Key of this filed will be stored in Food domain)
  2. Food
  3. User
  4. Customer (user and customer will have a relation)
  5. Order
  6. Order Status
  7. Order Details

By those domain models, we will separate it into 3 services such as

  1. Food Service (consists of Food Category & Food)
  2. User Service (consists of User & Customer)
  3. Order Service (consist of Order, Order Status & Order Details)

Important

In microservice, to connect with each service should have communication mechanism, so that we can share particular data that need by particular service. In this case order service will store kind of foods that booked by customer right by keeping Food ID and Customer ID. People don’t understand about ID, the things they need are Food name and Customer name maybe, but we don’t provide it in order service.

So, how we able to achieve it? Will we call the other service each time we need the data? In this approach we will use View Models. For instance we will create FoodViewModel and CustomerViewModel in order service, it’s like data replication. Instead of calling other service to get the detail of food and customer, we will take the benefit of view models. We can maintain the data in view model by using event driven. But it won’t be covered in this part.

Prerequisite

I hope we have understood about NET Core, Repository Pattern, MS SQL, MySQL and MongoDB

Let’s get started

A. Food Service

As per we mention above, that we will use 2 major domain such as Food Category and Food. This service will use MongoDB for the database. The following is standard folder structure of this service

I’m using docker and the image of MongoDB has been run as container. Then I login to MongoDB Compass, so we able to see we have 2 collections

The subsequent step I’m going to show about detail of code. But, make sure we have installed the following packages

  • MongoDB.Driver
  • Newtonsoft.Json
  1. appsettings.json (databse configuration will be stored here)
"DatabaseSettings": {
"FoodCategoriesCollectionName": "FoodCategories",
"FoodsCollectionName": "Foods",
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "RestaurantDB"
}

2. IDatabaseSettings.cs and DatabaseSettings.cs ( I’ll map the value from DatabaseSettings at appsettings into our class model)

public interface IDatabaseSettings
{
string DatabaseName { get; set; }
string ConnectionString { get; set; }
string FoodCategoriesCollectionName { get; set; }
string FoodsCollectionName { get; set; }
}

public class DatabaseSettings : IDatabaseSettings
{
public string DatabaseName { get; set; } = string.Empty;
public string ConnectionString { get; set; } = string.Empty;
public string FoodCategoriesCollectionName { get; set; } = string.Empty;
public string FoodsCollectionName { get; set; } = string.Empty;
}

Then we will map the value in program.cs. The benefits are, we able to map JSON value to object class and we register the service so that these object able to use by other class

builder.Services.Configure<DatabaseSettings>(
builder.Configuration.GetSection(nameof(DatabaseSettings)));

builder.Services.AddSingleton<IDatabaseSettings>(provider =>
provider.GetRequiredService<IOptions<DatabaseSettings>>().Value);

The thing need to keep in mind, I use “DatabaseSettings” for both class name and json section name

json section name
class name

3. FoodCategory.cs and Food.cs (these models are representation of document in Mongo DB’s collection)

public class FoodCategory
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = ObjectId.GenerateNewId().ToString();
public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public bool Deleted { get; set; }
}

public class Food
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = ObjectId.GenerateNewId().ToString();
public string CategoryCode { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Size { get; set; } = string.Empty;
public decimal Price { get; set; }
public decimal Discount { get; set; }
public uint MinQtyDiscount { get; set; }
public byte[] Image { get; set; } = default!;
public bool Deleted { get; set; }

}

4. Repositories

public interface IFoodCategoryRepository
{
Task<IEnumerable<FoodCategory>> GetAll();
Task<FoodCategory> Get(string id);
Task Save(FoodCategory category);
Task Delete(string id);
Task SoftDelete(string id);

}


public class FoodCategoryRepository : IFoodCategoryRepository
{
private readonly IMongoCollection<FoodCategory> _foodCategories;
public FoodCategoryRepository(IDatabaseSettings settings)
{
var client = new MongoClient(settings.ConnectionString);
var database = client.GetDatabase(settings.DatabaseName);
_foodCategories = database.GetCollection<FoodCategory>(settings.FoodCategoriesCollectionName);
}

public async Task Delete(string id)
{
await _foodCategories.DeleteOneAsync(d => d.Id == id);
}

public async Task<FoodCategory> Get(string id)
{
return await _foodCategories.Find(f => f.Id.Equals(id)).FirstOrDefaultAsync();
}

public async Task<IEnumerable<FoodCategory>> GetAll()
{
return await _foodCategories.Find(FilterDefinition<FoodCategory>.Empty).ToListAsync();
}

public async Task Save(FoodCategory category)
{
var data = await this.Get(category.Id);

if (data == null)
{
await _foodCategories.InsertOneAsync(category);
}
else
{
await _foodCategories.ReplaceOneAsync(r => r.Id.Equals(category.Id), category);
}
}

public async Task SoftDelete(string id)
{
var data = await Get(id);

if (data != null)
{
data.Deleted = true;
await _foodCategories.ReplaceOneAsync(r => r.Id == id, data);
}
}
}
public interface IFoodRepository
{
Task<IEnumerable<Food>> GetAll();
Task<IEnumerable<Food>> GetByCategory(string catgoryCode);
Task<Food> Get(string id);
Task Save(Food food);
Task Delete(string id);
Task SoftDelete(string id);
}

public class FoodRepository : IFoodRepository
{
private readonly IMongoCollection<Food> _foods;
public FoodRepository(IDatabaseSettings settings)
{
var client = new MongoClient(settings.ConnectionString);
var database = client.GetDatabase(settings.DatabaseName);
_foods = database.GetCollection<Food>(settings.FoodsCollectionName);
}

public async Task Delete(string id)
{
await _foods.DeleteOneAsync(d => d.Id == id);
}

public async Task<Food> Get(string id)
{
return await _foods.Find(f => f.Id == id).FirstOrDefaultAsync();
}

public async Task<IEnumerable<Food>> GetAll()
{
return await _foods.Find(FilterDefinition<Food>.Empty).ToListAsync();
}

public async Task<IEnumerable<Food>> GetByCategory(string catgoryCode)
{
return await _foods.Find(f => f.CategoryCode == catgoryCode).ToListAsync();
}

public async Task Save(Food food)
{
var data = await Get(food.Id);

if (data == null)
{
await _foods.InsertOneAsync(food);
}
else
{
await _foods.ReplaceOneAsync(r => r.Id == food.Id, food);
}
}

public async Task SoftDelete(string id)
{
var data = await Get(id);

if (data != null)
{
data.Deleted = true;
await _foods.ReplaceOneAsync(r => r.Id == id, data);
}
}
}

Don’t forget to register these repositories in DI Container that available in Program.cs

builder.Services.AddScoped<IFoodCategoryRepository, FoodCategoryRepository>();
builder.Services.AddScoped<IFoodRepository, FoodRepository>();

5. Controller (FoodCategoryController.cs and FoddController.cs)

[Route("api/[controller]")]
[ApiController]
public class FoodCategoryController : ControllerBase
{
private readonly IFoodCategoryRepository _foodCategoryRepository;
public FoodCategoryController(IFoodCategoryRepository foodCategoryRepository)
{
_foodCategoryRepository = foodCategoryRepository;
}

[HttpGet("GetAll")]
public async Task<IActionResult> GetAll()
{
var data = await _foodCategoryRepository.GetAll();

if (data == null || data.Count() < 1)
{
return NotFound();
}

return Ok(data);
}

[HttpGet("Get")]
public async Task<IActionResult> Get(string id)
{
var data = await _foodCategoryRepository.Get(id);

if (data == null)
{
return NotFound();
}

return Ok(data);
}

[HttpPost("Save")]
public async Task<IActionResult> Save([FromBody] FoodCategoryModel model)
{
FoodCategory foodCategory = new FoodCategory
{
Id = string.IsNullOrEmpty(model.Id) ? ObjectId.GenerateNewId().ToString() : model.Id,
Code = model.Code,
Name = model.Name,
Deleted = model.Deleted,
};

await _foodCategoryRepository.Save(foodCategory);

if (string.IsNullOrEmpty(model.Id))
{
return StatusCode(201);
}
else
{
return NoContent();
}

}

[HttpDelete("Delete")]
public async Task<IActionResult> Delete(string id)
{
await _foodCategoryRepository.Delete(id);
return NoContent();
}

[HttpDelete("SoftDelete")]
public async Task<IActionResult> SoftDelete(string id)
{
await _foodCategoryRepository.SoftDelete(id);
return NoContent();
}
}
[Route("api/[controller]")]
[ApiController]
public class FoodController : ControllerBase
{
private readonly IFoodRepository _foodRepository;
private readonly IEventPublisher _eventProducer;
public FoodController(IFoodRepository foodRepository, IEventPublisher eventProducer)
{
_foodRepository = foodRepository;
_eventProducer = eventProducer;
}

[HttpGet("GetAll")]
public async Task<IActionResult> GetAll()
{
var data = await _foodRepository.GetAll();

if (data == null || data.Count() < 1)
{
return NotFound();
}

return Ok(data);
}

[HttpGet("GetByCategory")]
public async Task<IActionResult> GetByCategory(string categoryCode)
{
var data = await _foodRepository.GetByCategory(categoryCode);

if (data == null || data.Count() < 1)
{
return NotFound();
}

return Ok(data);
}

[HttpGet("GetById/{id}")]
public async Task<IActionResult> GetById(string id)
{
var data = await _foodRepository.Get(id);

if (data == null)
{
return NotFound();
}

return Ok(data);
}

[HttpPost("Save")]
public async Task<IActionResult> Save([FromForm] FoodModel model)
{
Food food = new Food
{
Id = string.IsNullOrEmpty(model.Id) ? ObjectId.GenerateNewId().ToString() : model.Id,
CategoryCode = model.CategoryCode,
Name = model.Name,
Description = model.Description,
Size = model.Size,
Discount = model.Discount,
Price = model.Price,
Image = GetImage(model.Image),
Deleted = model.Deleted,
};

await _foodRepository.Save(food);

if (string.IsNullOrEmpty(model.Id))
{
_eventProducer.SendInsertFoodMessage(food);
return StatusCode(201);
}
else
{
return NoContent();
}


}

[HttpDelete("Delete")]
public async Task<IActionResult> Delete(string id)
{
await _foodRepository.Delete(id);
return NoContent();
}

[HttpDelete("SoftDelete")]
public async Task<IActionResult> SoftDelete(string id)
{
await _foodRepository.SoftDelete(id);
return NoContent();
}

private byte[] GetImage(IFormFile image)
{
if (image.Length > 0)
{
using (var ms = new MemoryStream())
{
image.CopyTo(ms);
var fileBytes = ms.ToArray();

return fileBytes;
}
}
else
{
return default!;
}
}
}

Before we proceed to next service, I’ll cover other things like Custom Models and Global Exception that you can see in my github repository.

Why we need custom model? Ideally model only represent table of database (Relational Database) or collection of database (NoSQL). So if we receive the data from client / front end or expose data to client, we will use different model though the properties of model and custom model are similar.

Why we need global exception? Instead of writing try catch on each action method in all controllers, we only need write it once and then register it in middleware.

B. User Service

User service has responsible to maintain all of the user and customer data and behavior. It will develop using MySQL database.

Notes :

  • Configuration Folder : it will set property using fluent API that will be applied in database
  • Migration will be auto created when we try to create migration process

Let’s run the docker image first

I have done with migration process, so able to see the result of MySQL database

Let’s code, But, make sure we have installed the following packages

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Tools
  • Pomelo.EntityFrameworkCore.MySql
  1. appsettings.json
"ConnectionStrings": {
"DefaultConnection": "server=localhost;port=3306;database=RestaurantUserDB;user=root;password=mysqladmin;Persist Security Info=False;Connect Timeout=300"
}

2. Models

public class User
{
public Guid Id { get; set; }
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public bool Deleted { get; set; }

public Customer Customer { get; set; } = default!;
}

public class Customer
{
public int Id { get; set; }
public Guid UserId { get; set; }
public string Name { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public uint Point { get; set; }
public bool Deleted { get; set; }

public User User { get; set; } = default!;
}

Then we will configure the attribute property using fluent API instead of Data Annotation (Just my preferences)

public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasKey(a => a.Id);
builder.Property(a => a.Username).IsRequired().HasMaxLength(50);
builder.Property(a => a.Password).IsRequired().HasMaxLength(250);

builder.HasQueryFilter(x => !x.Deleted);
}
}

public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.HasKey(a => a.Id);
builder.Property(a => a.UserId).IsRequired();
builder.Property(a => a.Name).IsRequired();
builder.Property(a => a.Address).IsRequired();
builder.Property(a => a.Phone).IsRequired();

builder.HasOne(e => e.User)
.WithOne(e => e.Customer)
.HasForeignKey<Customer>(e => e.UserId);

builder.HasQueryFilter(x => !x.Deleted);
}
}

3. Data

Here we only have single class called Application Context. Once we have the context don’t forget to register in Program.cs

public class ApplicationContext : DbContext
{
public ApplicationContext(DbContextOptions options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new UserConfiguration());
modelBuilder.ApplyConfiguration(new CustomerConfiguration());
}

public DbSet<User> Users { get; set; } = default!;
public DbSet<Customer> Customers { get; set; } = default!;
}
string mySqlConnectionStr = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContextPool<ApplicationContext>(options =>
options.UseMySql(mySqlConnectionStr, ServerVersion.AutoDetect(mySqlConnectionStr)));

4. Repositories

Here we have several class like interface (contract), class implementation and unit of work. Once classes have implemented the interfaces, then need to be registered on unit of work. Unit of work are repository wrapper, it has atomic function as well. For instance, we have 2 models / tables (user and customer), once the user data has saved into database, then need to save customer data as well. In common way, developers can use transaction to handle this case (if customer data get failure, then rollback the user data).

However using unit of work we don’t need any transactions as long as they are using single database.

Here are interfaces and class implementations

public interface IUserRepository
{
Task<IEnumerable<User>> GetAll();
Task<User?> GetById(Guid id);
Task<User?> GetAuth(string username, string password);
Task Save(User user);
Task Delete(Guid id);
Task SoftDelete(Guid id);
}


public class UserRepository : IUserRepository
{
private readonly ApplicationContext _context;
public UserRepository(ApplicationContext context)
{
_context = context;
}
public async Task Delete(Guid id)
{
var data = await this.GetById(id);

if (data != null)
{
_context.Users.Remove(data);
}

}

public async Task<IEnumerable<User>> GetAll()
{
return await _context.Users.AsNoTracking().ToListAsync();
}

public async Task<User?> GetAuth(string username, string password)
{
return await _context.Users.FirstOrDefaultAsync(f => f.Username.Equals(username) && f.Password.Equals(password));
}

public async Task<User?> GetById(Guid id)
{
return await _context.Users.SingleOrDefaultAsync(s => s.Id.Equals(id));
}

public async Task Save(User user)
{
var data = await this.GetById(user.Id);

if (data == null)
{
await _context.Users.AddAsync(user);
}
else
{
_context.Users.Update(user);
}
}

public async Task SoftDelete(Guid id)
{
var data = await this.GetById(id);

if (data != null)
{
data.Deleted = true;
}
}
}
public interface ICustomerRepository
{
Task<IEnumerable<Customer>> GetAll();
Task<Customer?> GetById(int id);
Task<Customer?> GetByUserId(Guid userId);
Task Save(Customer customer);
Task Delete(int id);
Task SoftDelete(int id);
Task SoftDelete(Guid userId);
}

public class CustomerRepository : ICustomerRepository
{
private readonly ApplicationContext _context;
public CustomerRepository(ApplicationContext context)
{
_context = context;
}
public async Task Delete(int id)
{
var data = await GetById(id);
if (data != null)
{
_context.Customers.Remove(data);
}

}

public async Task<IEnumerable<Customer>> GetAll()
{
return await _context.Customers.Include(i => i.User).AsNoTracking().ToListAsync();
}

public async Task<Customer?> GetById(int id)
{
return await _context.Customers.SingleOrDefaultAsync(s => s.Id == id);
}

public async Task<Customer?> GetByUserId(Guid userId)
{
return await _context.Customers.FirstOrDefaultAsync(f => f.UserId == userId);
}

public async Task Save(Customer customer)
{
var data = GetById(customer.Id);

if (data == null)
{
await _context.Customers.AddAsync(customer);
}
else
{
_context.Customers.Update(customer);
}
}

public async Task SoftDelete(int id)
{
var data = await this.GetById(id);

if (data != null)
{
data.Deleted = true;
}
}

public async Task SoftDelete(Guid userId)
{
var data = await this.GetByUserId(userId);

if (data != null)
{
data.Deleted = true;
}
}
}

Below are Unit of Work

public interface IUnitOfWork
{
IUserRepository UserRepository { get; }
ICustomerRepository CustomerRepository { get; }
Task<bool> SaveAsync();
}


public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationContext _context;
public UnitOfWork(ApplicationContext context)
{
_context = context;
}
public IUserRepository UserRepository => new UserRepository(_context);

public ICustomerRepository CustomerRepository => new CustomerRepository(_context);

public async Task<bool> SaveAsync()
{
return await _context.SaveChangesAsync() > 0;
}
}

Don’t forget to register unit of work in Dependency Injection Container (Program.cs)

builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

5. Controllers

[Route("api/[controller]")]
[ApiController]
public class UserCustomerController : ControllerBase
{
private readonly IUnitOfWork _service;
public UserCustomerController(IUnitOfWork service)
{
_service = service;
}

[HttpPost("PostUserCustomer")]
public async Task<IActionResult> PostUserCustomer([FromBody] UserCustomerModel model)
{
ResponseModel<UserCustomerModel> response = new ResponseModel<UserCustomerModel>();

User user = new User
{
Id = Guid.NewGuid(),
Username = model.Username,
Password = model.Password
};

Customer customer = new Customer
{
UserId = user.Id,
Name = model.Name,
Address = model.Address,
Phone = model.Phone,
Point = 0,
};

await _service.UserRepository.Save(user);
await _service.CustomerRepository.Save(customer);

bool result = await _service.SaveAsync();

if (result)
{
model.Id = user.Id;
response.Data = model;

return StatusCode(201, response);
}
else
{
throw new Exception("Something error happened when saving data");
}

}

[HttpGet("GetCustomerByUserId")]
public async Task<IActionResult> GetCustomerByUserId(Guid userId)
{
ResponseModel<CustomerModel> response = new ResponseModel<CustomerModel>();

var customer = await _service.CustomerRepository.GetByUserId(userId);

if (customer == null)
{
response.ErrorMessage = $"Not Found. User {userId} doesn't have customer relation data";

return NotFound(response);
}
else
{
response.Data = new CustomerModel
{
Id = customer.Id,
Name = customer.Name,
UserId = customer.UserId,
Address = customer.Address,
Phone = customer.Phone,
Point = customer.Point,
};

return Ok(response);
}
}

[HttpGet("GetAllUserCustomer")]
public async Task<IActionResult> GetAllUserCustomer()
{
ResponseModel<List<UserCustomerModel>> response = new ResponseModel<List<UserCustomerModel>>();

var data = await _service.CustomerRepository.GetAll();

if (data == null)
{
response.ErrorMessage = "List empty";

return NotFound(response);
}

List<UserCustomerModel> userCustomers = new List<UserCustomerModel>();

foreach(var item in data)
{
userCustomers.Add(new UserCustomerModel
{
Id = item.User.Id,
Username = item.User.Username,
Name = item.Name,
Address = item.Address,
Point = item.Point,
Phone = item.Phone
});
}

response.Data = userCustomers;

return Ok(response);
}

[HttpPut("PutUserCustomer")]
public async Task<IActionResult> PutUserCustomer([FromBody] UserCustomerModel model)
{
ResponseModel<UserCustomerModel> response = new ResponseModel<UserCustomerModel>();

User? user = await _service.UserRepository.GetById(model.Id);

Customer? customer = await _service.CustomerRepository.GetByUserId(model.Id);

if (user == null || customer == null)
{
response.ErrorMessage = $"Data {model.Id} Not Found";
return BadRequest(response);
}

user.Password = model.Password;

customer.Name = model.Name;
customer.Address = model.Address;
customer.Phone = model.Phone;
customer.Point = model.Point;

await _service.UserRepository.Save(user);
await _service.CustomerRepository.Save(customer);

bool result = await _service.SaveAsync();

if (result)
{
model.Id = user.Id;
response.Data = model;

return NoContent();
}
else
{
throw new Exception("Something error happened when updating data");
}
}

[HttpDelete("DeleteUserCustomerByUserId/{userid}")]
public async Task<IActionResult> DeleteUserCustomerByUserId(Guid userid)
{
ResponseModel<bool> response = new ResponseModel<bool>();

await _service.UserRepository.SoftDelete(userid);
await _service.CustomerRepository.SoftDelete(userid);

bool result = await _service.SaveAsync();

if (result)
{
return NoContent();
}
else
{
throw new Exception("Something error happened when deleting data");
}
}
}

Please take a note in “PostUserCustomer” action method

await _service.UserRepository.Save(user);
await _service.CustomerRepository.Save(customer);

bool result = await _service.SaveAsync();

The above code is atomic, we will call SaveAsync after processing Insert User and Customer. It will ensure that all data saves will be successful or nothing.

C. Order Service

Order service responsible for maintain order from customer. So, the domain model are Order, Order Status and Order Details. (Order Status just like data master, you can ignore it)

Basically, this service similar with User Service, it’s just using SQL Server as the database. Therefore please check in on my GitHub repository.

Before starting code, please make sure that we have installed the following packages

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

D. API Gateway

Definition : API gateway is entry point between client and backend service collection (API Management).

Why we need API Gateway

  1. Single endpoint. By implementing microservices, of course we will have so many application with different API’s URL right. So client (Web/Mobile) will hit API with different URL. To simplify the URL, we able to use API Gateway
  2. We want to protect our APIs from overuse and abuse, Then we can implement authentication and rate limiting here

We only focus in implementation, not definition. So let’s getstarted

First of all, install “Ocelot” packages from Nuget Packages (since we will use ocelot as gateway)

After installing ocelot, create ocelot.json file in root project folder. Within the file, we will work with 2 items such as “GlobalConfiguration” and “Routes”

The important thing don’t forget to register ocelot.json file in Program.cs

builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
builder.Services.AddOcelot(builder.Configuration);

ocelot.json file

"GlobalConfiguration": {
"BaseUrl": "https://localhost:7151"
},


"Routes": [
{
"UpstreamPathTemplate": "/api/Food/GetAll",
"UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/Food/GetAll",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 7126
}
]
},
{
"UpstreamPathTemplate": "/api/Food/GetByCategory?categoryCode={categoryCode}",
"UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/Food/GetByCategory?categoryCode={categoryCode}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 7126
}
]
},
{
"UpstreamPathTemplate": "/api/Food/GetById/{id}",
"UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/Food/GetById/{id}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 7126
}
]
},
{
"UpstreamPathTemplate": "/api/Food/Save",
"UpstreamHttpMethod": [ "Post" ],
"DownstreamPathTemplate": "/api/Food/Save",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 7126
}
]
},
{
"UpstreamPathTemplate": "/api/Food/SoftDelete?id={id}",
"UpstreamHttpMethod": [ "Delete" ],
"DownstreamPathTemplate": "/api/Food/SoftDelete?id={id}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 7126
}
]
}


]

Notes

  • UpstreamPathTemplate : define the API Gateway endpoint that receives request from client
  • UpstreamHttpMethod : Specifies HTTP verb tht being used by API Gateway’s endpoint
  • DownstreamPathTemplate : endpoint of microservice. UpstreamPathTemplate will receive request from the client, then API gateway will proceed to microservice’s endpoint
  • DownstreamScheme : Represents the protocol to communicate with the microservice
  • DownstreamHostAndPorts : specifies the URL and the port from the microservices that are going to get the requests.

Testing Scenario

In the API Gateway, we only configure API route for Food Service, The rest will be configure in separate time (just for testing). Before testing, we have to run multiple project in our visual studio.

Food Service has base URL : https://localhost:7126/

API Gateway base base URL : https://localhost:7151/

Let’s hit our Food Service API (https://localhost:7126/api/Food/GetAll), It will return all food list as per shown below

Now Let’s hit our API Gateway

Both are giving similar response. Because we have mapped between two URL route in ocelot.json file

Conclusion

In this part we have created microservices and focus on how to separate business domain in order to specify how many services need to be developed according to its responsibility. We also have shown that microservices can be built using different technology stack and the last one is developing simple API gateway. Hopefully this article part can give basic understanding how to implement microservice using ASP NET Core. Thank You for reading.

--

--