Building Scalable Microservices with .NET 8
Building Scalable Microservices with .NET 8
Microservices architecture has become the de facto standard for building scalable, maintainable enterprise applications. In this post, I'll share my experience implementing microservices with .NET 8 and the lessons learned from real-world deployments.
Architecture Overview
A well-designed microservices architecture in .NET 8 typically includes:
- API Gateway (Azure API Management or Ocelot)
- Service Discovery (Consul or Azure Service Fabric)
- Message Brokers (Azure Service Bus or RabbitMQ)
- Distributed Caching (Redis)
- Monitoring (Application Insights, Prometheus)
Setting Up a Microservice
Here's a basic microservice setup using .NET 8 Minimal APIs:
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<UserContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Add health checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<UserContext>();
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// Define endpoints
app.MapGet("/api/users", async (UserContext context) =>
{
return await context.Users.ToListAsync();
})
.WithName("GetUsers")
.WithOpenApi();
app.MapHealthChecks("/health");
app.Run();
Docker Configuration
Every microservice should be containerized. Here's a optimized Dockerfile:
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["UserService/UserService.csproj", "UserService/"]
RUN dotnet restore "UserService/UserService.csproj"
COPY . .
WORKDIR "/src/UserService"
RUN dotnet build "UserService.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "UserService.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "UserService.dll"]
Service Communication
Implement resilient communication patterns:
public class OrderService
{
private readonly HttpClient _httpClient;
private readonly ILogger<OrderService> _logger;
public OrderService(HttpClient httpClient, ILogger<OrderService> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<User?> GetUserAsync(int userId)
{
try
{
var response = await _httpClient.GetAsync($"/api/users/{userId}");
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<User>(json);
}
_logger.LogWarning("User service returned {StatusCode}", response.StatusCode);
return null;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Error calling user service");
return null;
}
}
}
Configuration with Azure Service Bus
For async communication, use message brokers:
// Program.cs
builder.Services.AddAzureServiceBus(builder.Configuration.GetConnectionString("ServiceBus"));
// Message handler
public class OrderCreatedHandler : IMessageHandler<OrderCreatedEvent>
{
public async Task Handle(OrderCreatedEvent message)
{
// Process order created event
await ProcessOrder(message.OrderId);
}
}
Health Checks and Monitoring
Implement comprehensive health checks:
builder.Services.AddHealthChecks()
.AddDbContextCheck<ApplicationContext>()
.AddAzureServiceBusQueue("orders-queue")
.AddRedis(builder.Configuration.GetConnectionString("Redis"))
.AddHttpHealthCheck("user-service", "https://user-service/health");
Deployment with Azure Container Apps
Deploy using Azure Container Apps for serverless containers:
# azure-container-app.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: user-service-config
data:
ASPNETCORE_ENVIRONMENT: "Production"
ConnectionStrings__DefaultConnection: "Server=..."
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: myregistry.azurecr.io/user-service:latest
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: user-service-config
Best Practices I've Learned
- Start with a monolith and extract services as needed
- Database per service - avoid shared databases
- API versioning is crucial for backward compatibility
- Circuit breaker pattern for resilient service calls
- Centralized logging with correlation IDs
- Infrastructure as Code with Terraform or ARM templates
Performance Considerations
- Use connection pooling for database connections
- Implement response caching where appropriate
- Consider CQRS pattern for read/write separation
- Use async/await properly to avoid blocking threads
Monitoring and Observability
// Add telemetry
builder.Services.AddApplicationInsightsTelemetry();
// Custom metrics
public class OrderMetrics
{
private static readonly Counter OrdersProcessed =
Meter.CreateCounter<int>("orders_processed_total");
public void IncrementOrdersProcessed() => OrdersProcessed.Add(1);
}
Conclusion
Building microservices with .NET 8 requires careful planning and adherence to proven patterns. The key is to:
- Design for failure and implement resilience patterns
- Monitor everything with proper observability
- Automate deployment and scaling
- Keep services focused and loosely coupled
These practices have helped me successfully deploy and maintain microservices architectures serving millions of requests daily. The investment in proper architecture pays dividends in scalability and maintainability.
What challenges have you faced when implementing microservices? Share your experiences in the comments!
Compartir este post