Back to Blog
microservices
microservices-patterns
February 1, 2024•10 min min read
Essential Microservices Patterns for .NET
Microservices architecture brings scalability and flexibility, but also complexity. These essential patterns will help you build robust, maintainable microservices in .NET.
1. API Gateway Pattern
Problem
Clients need to communicate with multiple microservices, leading to:- Multiple network calls
- Different protocols
- Authentication complexity
- Version management
Solution
Create a single entry point that routes requests to appropriate services.public class ApiGateway
{
private readonly IHttpClientFactory <em>httpClientFactory;
private readonly IAuthService </em>authService;
public async Task<IActionResult> RouteRequest(HttpRequest request)
{
// Authenticate
var user = await <em>authService.ValidateToken(request.Headers["Authorization"]);
// Route based on path
var targetService = DetermineTargetService(request.Path);
var client = </em>httpClientFactory.CreateClient(targetService);
// Add common headers
client.DefaultRequestHeaders.Add("X-User-Id", user.Id);
client.DefaultRequestHeaders.Add("X-Request-Id", Guid.NewGuid().ToString());
// Forward request
var response = await client.SendAsync(CreateProxyRequest(request));
return new ProxyResult(response);
}
}
Using Ocelot
{
"Routes": [
{
"DownstreamPathTemplate": "/api/products/{everything}",
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{ "Host": "product-service", "Port": 80 }
],
"UpstreamPathTemplate": "/products/{everything}",
"UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE" ],
"AuthenticationOptions": {
"AuthenticationProviderKey": "Bearer"
}
}
]
}
2. Circuit Breaker Pattern
Problem
When a service fails, cascading failures can bring down the entire system.Solution
Monitor failures and "open the circuit" to prevent calls to failing services.using Polly;
using Polly.CircuitBreaker;</p><p>public class ResilientHttpClient
{
private readonly AsyncCircuitBreakerPolicy <em>circuitBreakerPolicy;
public ResilientHttpClient()
{
</em>circuitBreakerPolicy = Policy
.Handle<HttpRequestException>()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (exception, duration) =>
{
Console.WriteLine($"Circuit broken for {duration.TotalSeconds}s");
},
onReset: () =>
{
Console.WriteLine("Circuit reset");
}
);
}
public async Task<T> GetAsync<T>(string url)
{
return await <em>circuitBreakerPolicy.ExecuteAsync(async () =>
{
var client = new HttpClient();
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<T>();
});
}
}
Advanced: Fallback Strategy
var fallbackPolicy = Policy<ProductDto>
.Handle<BrokenCircuitException>()
.FallbackAsync(
fallbackValue: GetCachedProduct(),
onFallbackAsync: async (result) =>
{
Console.WriteLine("Using cached data due to circuit break");
}
);</p><p>var product = await fallbackPolicy.ExecuteAsync(
() => </em>client.GetAsync<ProductDto>($"/api/products/{id}")
);
3. Event-Driven Pattern
Problem
Services need to communicate without tight coupling.Solution
Use events and message brokers for asynchronous communication.// Domain Event
public class OrderPlacedEvent
{
public Guid OrderId { get; set; }
public Guid CustomerId { get; set; }
public decimal TotalAmount { get; set; }
public DateTime PlacedAt { get; set; }
}</p><p>// Publisher
public class OrderService
{
private readonly IMessageBus <em>messageBus;
public async Task PlaceOrderAsync(Order order)
{
// Save order
await </em>repository.AddAsync(order);
// Publish event
await <em>messageBus.PublishAsync(new OrderPlacedEvent
{
OrderId = order.Id,
CustomerId = order.CustomerId,
TotalAmount = order.TotalAmount,
PlacedAt = DateTime.UtcNow
});
}
}</p><p>// Subscriber - Inventory Service
public class InventoryEventHandler : IEventHandler<OrderPlacedEvent>
{
public async Task HandleAsync(OrderPlacedEvent @event)
{
// Reserve inventory
await </em>inventoryService.ReserveItems(@event.OrderId);
}
}</p><p>// Subscriber - Notification Service
public class NotificationEventHandler : IEventHandler<OrderPlacedEvent>
{
public async Task HandleAsync(OrderPlacedEvent @event)
{
// Send confirmation email
await <em>emailService.SendOrderConfirmation(@event.OrderId);
}
}
Using MassTransit with RabbitMQ
// Configure MassTransit
services.AddMassTransit(x =>
{
x.AddConsumer<OrderPlacedConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host("rabbitmq://localhost", h =>
{
h.Username("guest");
h.Password("guest");
});
cfg.ReceiveEndpoint("order-placed-queue", e =>
{
e.ConfigureConsumer<OrderPlacedConsumer>(context);
});
});
});
4. Saga Pattern
Problem
Distributed transactions across multiple services are complex.Solution
Use choreography or orchestration to manage long-running transactions.Choreography Approach
public class OrderSaga
{
// Step 1: Order created
public async Task Handle(OrderCreatedEvent @event)
{
await </em>messageBus.PublishAsync(new ReserveInventoryCommand
{
OrderId = @event.OrderId,
Items = @event.Items
});
}
// Step 2: Inventory reserved
public async Task Handle(InventoryReservedEvent @event)
{
await <em>messageBus.PublishAsync(new ProcessPaymentCommand
{
OrderId = @event.OrderId,
Amount = @event.TotalAmount
});
}
// Step 3: Payment processed
public async Task Handle(PaymentProcessedEvent @event)
{
await </em>messageBus.PublishAsync(new ShipOrderCommand
{
OrderId = @event.OrderId
});
}
// Compensation: Payment failed
public async Task Handle(PaymentFailedEvent @event)
{
await <em>messageBus.PublishAsync(new ReleaseInventoryCommand
{
OrderId = @event.OrderId
});
}
}
5. Service Discovery
Problem
Services need to find each other in dynamic environments.Solution
Use a service registry like Consul or Eureka.public class ConsulServiceRegistry
{
private readonly IConsulClient </em>consul;
public async Task RegisterServiceAsync(ServiceRegistration registration)
{
await <em>consul.Agent.ServiceRegister(new AgentServiceRegistration
{
ID = registration.Id,
Name = registration.Name,
Address = registration.Address,
Port = registration.Port,
Check = new AgentServiceCheck
{
HTTP = $"http://{registration.Address}:{registration.Port}/health",
Interval = TimeSpan.FromSeconds(10),
Timeout = TimeSpan.FromSeconds(5)
}
});
}
public async Task<ServiceEndpoint> DiscoverServiceAsync(string serviceName)
{
var services = await </em>consul.Health.Service(serviceName, tag: null, passingOnly: true);
if (!services.Response.Any())
throw new ServiceNotFoundException(serviceName);
// Load balance - pick random
var service = services.Response[Random.Shared.Next(services.Response.Length)];
return new ServiceEndpoint
{
Address = service.Service.Address,
Port = service.Service.Port
};
}
}
6. CQRS in Microservices
Separate Read and Write Models
// Command Side (Write)
public class CreateOrderCommand : IRequest<Guid>
{
public Guid CustomerId { get; set; }
public List<OrderItem> Items { get; set; }
}</p><p>public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly OrderDbContext <em>dbContext;
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken ct)
{
var order = new Order(request.CustomerId, request.Items);
</em>dbContext.Orders.Add(order);
await <em>dbContext.SaveChangesAsync(ct);
return order.Id;
}
}</p><p>// Query Side (Read)
public class GetOrderQuery : IRequest<OrderDto>
{
public Guid OrderId { get; set; }
}</p><p>public class GetOrderHandler : IRequestHandler<GetOrderQuery, OrderDto>
{
private readonly IOrderReadRepository </em>readRepo;
public async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken ct)
{
return await _readRepo.GetByIdAsync(request.OrderId);
}
}
Best Practices
✅ Start with a monolith: Don't jump to microservices too early ✅ Use async communication: Prefer events over synchronous HTTP ✅ Implement proper logging: Distributed tracing is essential ✅ Design for failure: Use circuit breakers and retries ✅ Automate everything: CI/CD is crucial for microservices ✅ Monitor health: Implement health checks and metrics
Conclusion
These patterns form the foundation of successful microservices architecture in .NET. Start with the patterns that solve your immediate problems:
- API Gateway for client communication
- Circuit Breaker for resilience
- Event-Driven for loose coupling
- Saga for distributed transactions
Tags
Microservices Patterns .NET
Subscribe to Newsletter
Get notified about new articles