Microservices Architecture in .NET Core

Microservices Architecture is a design approach that structures an application as a collection of loosely coupled, independently deployable services. Each service is responsible for a specific business capability and can be developed, deployed, and scaled independently.

In this blog, we’ll explore how to implement a Microservices Architecture in .NET Core using a Product and Order service as an example. We’ll discuss the key components, show how to set up and develop the services, and provide code snippets for each step.

What is Microservices Architecture?

Microservices Architecture breaks down a monolithic application into a set of smaller, autonomous services. Each service.

  • Owns its own data and database.
  • Communicates with other services through APIs (often via HTTP/REST or messaging systems).
  • Can be deployed independently, allowing for more flexibility in scaling and updating specific parts of an application.

Microservices typically focus on specific business domains (e.g., Product Management, Order Management), enabling teams to work on different services in parallel without affecting the entire application.

Key Concepts in Microservices Architecture

  1. Service Independence: Each microservice operates independently with its own database, business logic, and API.
  2. Inter-Service Communication: Microservices communicate with each other using lightweight protocols such as HTTP/REST or messaging queues (e.g., RabbitMQ).
  3. API Gateway: A single entry point that aggregates requests to multiple microservices, handling authentication, routing, and rate limiting.
  4. Service Discovery: Automatically detects and manages the network locations of service instances.
  5. Distributed Data Management: Each microservice manages its own data storage, ensuring data consistency and availability.
  6. Resilience and Fault Tolerance: The architecture should handle failures gracefully, using techniques like circuit breakers, retries, and health checks.

Setting up the Microservices

Let's create two microservices: ProductService and OrderService. Each will have its own database, business logic, and API.

1. Product Service

Project Setup

  • Create a new ASP.NET Core Web API project named ProductService.
  • Add the necessary models, controllers, and data access classes.

Product Model

public class Product
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

DbContext

public class ProductDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    public ProductDbContext(DbContextOptions<ProductDbContext> options) : base(options) { }
}

Product Controller

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    private readonly ProductDbContext _context;
    public ProductsController(ProductDbContext context)
    {
        _context = context;
    }
    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(Guid id)
    {
        var product = await _context.Products.FindAsync(id);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }
    [HttpPost]
    public async Task<IActionResult> CreateProduct([FromBody] Product product)
    {
        _context.Products.Add(product);
        await _context.SaveChangesAsync();
        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
    }
    // Additional actions...
}

Program. cs / Startup.cs

  • Configure the ProductDbContext and add necessary services.
  • Enable the use of controllers and other middleware.

2. Order Service

Project Setup

  • Create another ASP.NET Core Web API project named OrderService.
  • Add the necessary models, controllers, and data access classes.

Order Model

public class Order
{
    public Guid Id { get; set; }
    public Guid ProductId { get; set; }
    public int Quantity { get; set; }
    public DateTime OrderDate { get; set; }
}

DbContext

public class OrderDbContext : DbContext
{
    public DbSet<Order> Orders { get; set; }
    public OrderDbContext(DbContextOptions<OrderDbContext> options) : base(options) { }
}

Order Controller

[Route("api/[controller]")]
[ApiController]
public class OrdersController : ControllerBase
{
    private readonly OrderDbContext _context;
    public OrdersController(OrderDbContext context)
    {
        _context = context;
    }
    [HttpGet("{id}")]
    public async Task<IActionResult> GetOrder(Guid id)
    {
        var order = await _context.Orders.FindAsync(id);
        if (order == null)
        {
            return NotFound();
        }
        return Ok(order);
    }
    [HttpPost]
    public async Task<IActionResult> CreateOrder([FromBody] Order order)
    {
        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
        return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
    }
    // Additional actions...
}

Program.cs / Startup.cs

  • Configure the OrderDbContext and add necessary services.
  • Enable the use of controllers and other middleware.

3. Inter-Service Communication

Since the Order service needs to interact with the Product service (e.g., to validate product availability), we need to set up communication between these services.

Using HTTP Client: Add an HttpClient to the OrderService to call the ProductService.

Example

public class ProductClient
{
    private readonly HttpClient _httpClient;
    public ProductClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    public async Task<Product> GetProductAsync(Guid productId)
    {
        var response = await _httpClient.GetAsync($"http://localhost:5001/api/products/{productId}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsAsync<Product>();
    }
}

Register HttpClient in Startup. cs for dependency injection.

4. API Gateway

For a real-world application, you'd typically introduce an API Gateway to act as a single entry point for all client requests. This gateway routes requests to the appropriate microservice and can also handle cross-cutting concerns like authentication, logging, and rate limiting.

Example

  • Use Ocelot, a popular API Gateway for .NET Core.
  • Configure Ocelot to route requests to ProductService and OrderService.

Ocelot Configuration

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/products/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5001
        }
      ],
      "UpstreamPathTemplate": "/products/{everything}",
      "UpstreamHttpMethod": [ "GET", "POST" ]
    },
    {
      "DownstreamPathTemplate": "/api/orders/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5002
        }
      ],
      "UpstreamPathTemplate": "/orders/{everything}",
      "UpstreamHttpMethod": [ "GET", "POST" ]
    }
  ]
}

5. Dockerizing Microservices

To deploy each microservice independently, you can containerize them using Docker.

Dockerfile for ProductService

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["ProductService/ProductService.csproj", "ProductService/"]
RUN dotnet restore "ProductService/ProductService.csproj"
COPY . .
WORKDIR "/src/ProductService"
RUN dotnet build "ProductService.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "ProductService.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ProductService.dll"]

Dockerfile for OrderService: Similar to the above with paths changed for OrderService.

Docker Compose

version: '3.4'
services:
  productservice:
    image: productservice
    build:
      context: .
      dockerfile: ProductService/Dockerfile
    ports:
      - "5001:80"
  orderservice:
    image: orderservice
    build:
      context: .
      dockerfile: OrderService/Dockerfile
    ports:
      - "5002:80"

6. Testing and Deployment

  • Unit Testing: Write unit tests for each microservice to validate functionality.
  • Deployment: Deploy the microservices using Docker Compose, Kubernetes, or another orchestrator. Ensure each service is running independently and can communicate with the others.

Conclusion

In this blog, we've walked through the implementation of a Microservices Architecture in .NET Core using a Product and Order service as examples. By breaking down the application into smaller, manageable services, we’ve created a system that is scalable, maintainable, and resilient to changes.

Microservices Architecture is ideal for complex applications where different parts of the system need to evolve independently. While it introduces complexity in managing distributed systems, the benefits in terms of flexibility and scalability often outweigh the costs.

By following the steps outlined in this blog, you can start building microservices in .NET Core, setting up each service with its own database, API, and business logic, and ensuring they work together seamlessly in a distributed environment.