Isaac.

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

  1. Separate databases: No shared data stores
  2. Async communication: Use events and messaging
  3. Independent deployment: Version APIs carefully
  4. Circuit breakers: Handle service failures
  5. 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.