.NET Microservices
Introduction
Microservices architecture has revolutionized how we design and deploy applications in the modern cloud era. Unlike traditional monolithic applications where all components are tightly integrated into a single unit, microservices break down applications into smaller, independently deployable services that communicate with each other over a network.
.NET provides robust support for building microservices through ASP.NET Core, making it an excellent choice for developers looking to embrace this architectural pattern. In this guide, we'll explore what microservices are, why they're beneficial, and how to implement them using .NET technologies.
What are Microservices?
Microservices are an architectural style where an application is composed of small, independent services that:
- Are focused on doing one thing well
- Run in their own process
- Communicate via lightweight mechanisms (often HTTP/REST or messaging)
- Can be deployed independently
- Can be written in different programming languages
- Can use different data storage technologies
Here's a visual comparison between monolithic and microservices architectures:
Monolithic Architecture:
- Single codebase
- Single deployment unit
- Shared database
- Tightly coupled components
Microservices Architecture:
- Multiple codebases
- Independent deployment units
- Decentralized data management
- Loosely coupled components
Why .NET for Microservices?
.NET offers several advantages for building microservices:
- Cross-platform: .NET Core/.NET 5+ runs on Windows, Linux, and macOS
- Performance: High performance HTTP processing with Kestrel
- Docker support: Excellent containerization capabilities
- API tools: Built-in support for RESTful APIs and gRPC
- Service discovery: Integration with service discovery tools
- Configuration: Robust configuration management
- Resilience libraries: Support for resilience patterns through libraries like Polly
Building Your First Microservice with .NET
Let's create a simple microservice using ASP.NET Core:
Step 1: Set up a new ASP.NET Core Web API project
dotnet new webapi -n ProductService
cd ProductService
Step 2: Define a model
Create a Product.cs
file:
namespace ProductService.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
}
Step 3: Create a controller
Create a ProductController.cs
file:
using Microsoft.AspNetCore.Mvc;
using ProductService.Models;
using System.Collections.Generic;
using System.Linq;
namespace ProductService.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private static List<Product> _products = new List<Product>
{
new Product { Id = 1, Name = "Laptop", Price = 999.99m, Stock = 50 },
new Product { Id = 2, Name = "Smartphone", Price = 499.99m, Stock = 100 },
new Product { Id = 3, Name = "Headphones", Price = 99.99m, Stock = 200 }
};
[HttpGet]
public ActionResult<IEnumerable<Product>> GetAll()
{
return _products;
}
[HttpGet("{id}")]
public ActionResult<Product> GetById(int id)
{
var product = _products.FirstOrDefault(p => p.Id == id);
if (product == null)
{
return NotFound();
}
return product;
}
[HttpPost]
public ActionResult<Product> Create(Product product)
{
product.Id = _products.Max(p => p.Id) + 1;
_products.Add(product);
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
}
}
Step 4: Run your microservice
dotnet run
Your microservice will be running and accessible at https://localhost:5001/api/products.
Communication Between Microservices
Microservices need to communicate with each other. Here are common approaches in .NET:
HTTP Communication using HttpClient
// In OrderService, calling ProductService
public class ProductClient
{
private readonly HttpClient _httpClient;
public ProductClient(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("https://localhost:5001/");
}
public async Task<Product> GetProductAsync(int id)
{
return await _httpClient.GetFromJsonAsync<Product>($"api/products/{id}");
}
}
// In Startup.cs:
services.AddHttpClient<ProductClient>();
Message-Based Communication with RabbitMQ
For event-based communication, we can use RabbitMQ with the RabbitMQ.Client
package:
dotnet add package RabbitMQ.Client
Sending a message:
// In OrderService when a new order is created
public class OrderCreatedPublisher
{
public void PublishOrderCreated(Order order)
{
var factory = new ConnectionFactory() { HostName = "localhost" };
using (var connection = factory.CreateConnection())
using (var channel = connection.CreateModel())
{
channel.QueueDeclare(queue: "orders",
durable: false,
exclusive: false,
autoDelete: false,
arguments: null);
string message = JsonSerializer.Serialize(order);
var body = Encoding.UTF8.GetBytes(message);
channel.BasicPublish(exchange: "",
routingKey: "orders",
basicProperties: null,
body: body);
}
}
}
Receiving a message:
// In InventoryService to process orders
public class OrderCreatedConsumer
{
public void Start()
{
var factory = new ConnectionFactory() { HostName = "localhost" };
using (var connection = factory.CreateConnection())
using (var channel = connection.CreateModel())
{
channel.QueueDeclare(queue: "orders",
durable: false,
exclusive: false,
autoDelete: false,
arguments: null);
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
var order = JsonSerializer.Deserialize<Order>(message);
// Process the order
Console.WriteLine($"Received order: {order.Id}");
};
channel.BasicConsume(queue: "orders",
autoAck: true,
consumer: consumer);
Console.WriteLine("Waiting for orders...");
Console.ReadLine();
}
}
}
Containerizing .NET Microservices with Docker
Docker containers are a natural fit for microservices. Let's containerize our ProductService:
Step 1: Create a Dockerfile
Create a file named Dockerfile
in your project root:
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["ProductService.csproj", "./"]
RUN dotnet restore "ProductService.csproj"
COPY . .
WORKDIR "/src"
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"]
Step 2: Build the Docker image
docker build -t product-service .
Step 3: Run the Docker container
docker run -d -p 8080:80 --name product-service-container product-service
Your microservice is now running in a container and accessible at http://localhost:8080/api/products.
Implementing a Complete Microservices Solution
Let's create a more complete example with multiple services:
- Product Service - Manages product inventory
- Order Service - Handles customer orders
- API Gateway - Routes external requests to appropriate services
API Gateway with YARP
YARP (Yet Another Reverse Proxy) is a modern, high-performance reverse proxy developed by Microsoft.
dotnet new webapi -n ApiGateway
cd ApiGateway
dotnet add package Microsoft.ReverseProxy
Update Program.cs
:
var builder = WebApplication.CreateBuilder(args);
// Add reverse proxy capability
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();
Add to appsettings.json
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ReverseProxy": {
"Routes": {
"products-route": {
"ClusterId": "products-cluster",
"Match": {
"Path": "/products/{**catch-all}"
}
},
"orders-route": {
"ClusterId": "orders-cluster",
"Match": {
"Path": "/orders/{**catch-all}"
}
}
},
"Clusters": {
"products-cluster": {
"Destinations": {
"products-api": {
"Address": "http://localhost:5001/api/products/"
}
}
},
"orders-cluster": {
"Destinations": {
"orders-api": {
"Address": "http://localhost:5002/api/orders/"
}
}
}
}
}
}
Best Practices for .NET Microservices
-
Keep services small and focused: Each microservice should have a single responsibility.
-
Design for failure: Use resilience patterns like Circuit Breaker with Polly:
// Add Polly to service client
services.AddHttpClient<ProductClient>()
.AddTransientHttpErrorPolicy(builder => builder.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(30)
));
- Use health checks: Implement health checks for each service:
// In Program.cs
builder.Services.AddHealthChecks();
// In app configuration
app.MapHealthChecks("/health");
- Implement proper logging: Use structured logging with Serilog:
// In Program.cs
builder.Host.UseSerilog((ctx, lc) => lc
.WriteTo.Console()
.WriteTo.File("logs/app.log", rollingInterval: RollingInterval.Day));
-
Use configuration management: Externalize configuration with environment variables or a configuration server.
-
Implement service discovery: Use tools like Consul or built-in Kubernetes discovery.
-
Design proper database strategy: Each service should have its own database or schema.
Real-World Example: E-commerce Microservices
Let's look at how an e-commerce application might be broken down into microservices:
- Product Catalog Service: Manages product listings
- Inventory Service: Tracks stock levels
- Order Service: Processes customer orders
- Payment Service: Handles payment processing
- Customer Service: Manages customer information
- Notification Service: Sends emails and notifications
Here's how the order process might flow through these services:
- User places an order in the UI
- API Gateway routes request to Order Service
- Order Service checks with Inventory Service if items are available
- Order Service requests payment from Payment Service
- If payment succeeds, Order Service confirms the order
- Inventory Service updates stock levels
- Notification Service sends order confirmation to customer
Summary
.NET microservices offer a powerful approach to building scalable, maintainable, and resilient applications. In this guide, we've covered:
- The fundamentals of microservices architecture
- How to create a basic microservice with ASP.NET Core
- Communication patterns between microservices
- Containerization with Docker
- API Gateway implementation
- Best practices for microservice development
As you begin your journey with .NET microservices, remember that this architecture isn't suitable for every scenario. Small applications might benefit from a simpler monolithic approach. However, for complex, evolving applications that need scalability, team autonomy, and technology flexibility, microservices can offer significant advantages.
Additional Resources
-
Documentation
-
Books
- ".NET Microservices: Architecture for Containerized .NET Applications" by Microsoft
- "Building Microservices" by Sam Newman
-
Tools
- Docker and Docker Compose
- Kubernetes for orchestration
- YARP for API Gateway
- Polly for resilience patterns
Exercises
- Create a basic microservice that manages a collection of books with CRUD operations.
- Add a second microservice that handles user reviews for books and make them communicate.
- Implement health checks and logging in your microservices.
- Containerize your microservices using Docker and run them together using Docker Compose.
- Implement an API Gateway to route requests to your book and review services.
By completing these exercises, you'll gain practical experience with the key concepts of microservices architecture in .NET.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)