Microservices with ASP.NET Core
Design and implement scalable microservices architecture.
By EMEPublished: February 20, 2025
microservicesaspnet coredistributed systemsarchitecture
A Simple Analogy
Microservices is like franchising a business. Each location (service) operates independently but coordinates with others. If one fails, others keep running.
Why Microservices?
- Scalability: Scale individual services independently
- Flexibility: Use different tech stacks per service
- Resilience: One service failure doesn't kill system
- Deployment: Deploy services independently
- Team autonomy: Teams own their services
Service Structure
// OrderService/Controllers/OrdersController.cs
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
private readonly IPublishEndpoint _publishEndpoint;
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
{
var order = await _orderService.CreateAsync(request);
// Publish event for other services
await _publishEndpoint.Publish(new OrderCreated
{
OrderId = order.Id,
CustomerId = order.CustomerId
});
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id)
{
var order = await _orderService.GetAsync(id);
if (order == null) return NotFound();
return Ok(order);
}
}
// OrderService/Services/OrderService.cs
public class OrderService : IOrderService
{
private readonly IOrderRepository _repository;
private readonly IPaymentClient _paymentClient;
public async Task<OrderDto> CreateAsync(CreateOrderRequest request)
{
// Validate
if (!request.Items.Any()) throw new ArgumentException("No items");
// Call payment service
var paymentResponse = await _paymentClient.AuthorizeAsync(request.PaymentInfo);
if (!paymentResponse.Success) throw new InvalidOperationException("Payment failed");
// Create order
var order = new Order
{
CustomerId = request.CustomerId,
Items = MapItems(request.Items),
Status = OrderStatus.Confirmed
};
await _repository.SaveAsync(order);
return MapToDto(order);
}
}
Service Discovery
// HttpClientFactory with Polly
builder.Services.AddHttpClient<IPaymentClient, PaymentClient>()
.ConfigureHttpClient(client =>
{
client.BaseAddress = new Uri("https://payment-service:5001");
})
.AddTransientHttpErrorPolicy()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
// Or with service discovery
builder.Services.AddHttpClient<IPaymentClient, PaymentClient>()
.ConfigureHttpClient((sp, client) =>
{
var serviceRegistry = sp.GetRequiredService<IServiceRegistry>();
var paymentService = serviceRegistry.GetService("payment-service");
client.BaseAddress = new Uri($"https://{paymentService.Host}:{paymentService.Port}");
});
API Gateway Pattern
// Gateway/Program.cs
var app = builder.Build();
// Route to OrderService
app.MapGet("/api/orders/{id}", async (int id, HttpClient http) =>
{
var response = await http.GetAsync($"https://order-service/api/orders/{id}");
return response;
});
// Route to PaymentService
app.MapPost("/api/payments", async (PaymentRequest request, HttpClient http) =>
{
var json = JsonSerializer.Serialize(request);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await http.PostAsync("https://payment-service/api/payments", content);
return response;
});
// Or use Ocelot NuGet
var configuration = new ConfigurationBuilder()
.AddJsonFile("ocelot.json")
.Build();
builder.Services.AddOcelot(configuration);
Saga Pattern for Transactions
// Distributed transaction across services
public partial class OrderFulfillmentSaga : StateMachine<OrderFulfillmentState>
{
public Event<OrderSubmitted> Submit { get; private set; }
public OrderFulfillmentSaga()
{
Initially(
When(Submit)
.Then(context =>
{
context.Instance.OrderId = context.Data.OrderId;
})
.TransitionTo(PaymentProcessing)
.Send(context => new Uri("rabbitmq://payment-service"),
new ProcessPayment { OrderId = context.Data.OrderId })
);
}
}
Monitoring Microservices
// Distributed tracing
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter());
// Health checks for dependencies
builder.Services.AddHealthChecks()
.AddCheck<PaymentServiceHealthCheck>("payment-service")
.AddCheck<OrderDatabaseHealthCheck>("order-db");
Best Practices
- Separate databases: No shared data stores
- Async communication: Use events and messaging
- Independent deployment: Version APIs carefully
- Circuit breakers: Handle service failures
- Distributed tracing: Track requests across services
Related Concepts
- API Gateway pattern
- Service mesh (Istio, Linkerd)
- Container orchestration (Kubernetes)
- Event-driven architecture
- CQRS for scalable queries
Summary
Microservices enable scalable, resilient systems by decomposing monoliths. Use service discovery, API gateways, and async communication to coordinate independent services.