Deep Dive: Saga Pattern & CQRS in .NET Core with Azure
Let’s break down Saga Pattern and CQRS in detail, focusing on .NET Core implementations and Azure services.
1. Saga Pattern: Distributed Transactions
Problem
In microservices, a single business transaction (e.g., "Place Order") spans multiple services (Order, Payment, Inventory). Traditional ACID transactions don’t work across services.
Solution: Saga
A saga is a sequence of local transactions, where each step has a compensating action (undo operation) if something fails.
Two Saga Approaches
A. Choreography (Event-Driven)
How it works: Services emit events (e.g.,
OrderPlaced,PaymentProcessed) and react to them.Pros: Decentralized, no single point of failure.
Cons: Complex to debug (event spaghetti).
B. Orchestration (Central Coordinator)
How it works: A central orchestrator (e.g., Azure Durable Functions) controls the flow.
Pros: Easier to manage, explicit workflow.
Cons: Orchestrator can become a bottleneck.
Implementing Saga in .NET Core + Azure
Option 1: Choreography with Azure Service Bus
// OrderService (Publishes OrderPlaced event) await _serviceBusSender.SendMessageAsync(new OrderPlacedEvent { OrderId = orderId }); // PaymentService (Listens to OrderPlaced, processes payment, then emits PaymentProcessed) _serviceBusProcessor.ProcessMessageAsync += async (args) => { var orderPlaced = args.Message.Body.ToObjectFromJson<OrderPlacedEvent>(); await _paymentService.ProcessPayment(orderPlaced.OrderId); await _serviceBusSender.SendMessageAsync(new PaymentProcessedEvent { OrderId = orderPlaced.OrderId }); };
Option 2: Orchestration with Azure Durable Functions
[FunctionName("OrderSaga")] public static async Task RunOrchestrator( [OrchestrationTrigger] IDurableOrchestrationContext context) { var orderId = context.GetInput<string>(); try { // Step 1: Reserve Inventory await context.CallActivityAsync("ReserveInventory", orderId); // Step 2: Process Payment await context.CallActivityAsync("ProcessPayment", orderId); // Step 3: Finalize Order await context.CallActivityAsync("FinalizeOrder", orderId); } catch (Exception) { // Compensating transactions await context.CallActivityAsync("CancelPayment", orderId); await context.CallActivityAsync("RestoreInventory", orderId); } }
Azure Services Used
Choreography: Azure Service Bus / Event Grid.
Orchestration: Azure Durable Functions.
Compensation Logic: Stored in each service.
2. CQRS (Command Query Responsibility Segregation)
Problem
Reads and writes have different scalability needs.
Complex queries slow down write operations.
Solution: CQRS
Commands (Writes): Modify state (e.g.,
CreateOrder).Queries (Reads): Fetch data (e.g.,
GetOrderDetails).Separate Databases: Optimized for each operation.
Implementing CQRS in .NET Core + Azure
A. Simple CQRS with MediatR
// Command (Write) public class CreateOrderCommand : IRequest<Guid> { public string ProductId { get; set; } } public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid> { public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken ct) { var order = new Order { ProductId = request.ProductId }; _dbContext.Orders.Add(order); await _dbContext.SaveChangesAsync(ct); return order.Id; } } // Query (Read) public class GetOrderQuery : IRequest<OrderDto> { public Guid OrderId { get; set; } } public class GetOrderHandler : IRequestHandler<GetOrderQuery, OrderDto> { public async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken ct) { return await _dbContext.Orders .Where(o => o.Id == request.OrderId) .ProjectTo<OrderDto>() .FirstOrDefaultAsync(ct); } }
B. Advanced CQRS with Separate Read/Write Databases
Write DB (OLTP): Azure SQL (normalized schema).
Read DB (OLAP): Azure Cosmos DB (denormalized for fast queries).
Syncing Data: Use Azure Event Grid or Change Feed in Cosmos DB.
// When an order is created in SQL, publish an event to update Cosmos DB _serviceBusSender.Publish(new OrderCreatedEvent { OrderId = order.Id });
Azure Services Used
Command DB: Azure SQL.
Query DB: Cosmos DB.
Event Syncing: Azure Service Bus / Event Grid.
Comparison: Saga vs. CQRS
| Aspect | Saga | CQRS |
|---|---|---|
| Purpose | Manage distributed transactions | Separate read/write operations |
| Data Consistency | Eventual consistency | Immediate (write) + eventual (read) |
| Azure Tools | Durable Functions, Service Bus | Cosmos DB, Event Grid, MediatR |
| Complexity | High (compensation logic) | Medium (dual models) |
When to Use Which?
Use Saga when you need transactions across microservices (e.g., e-commerce checkout).
Use CQRS when reads and writes have different scaling needs (e.g., dashboard analytics).
Final Architecture Example (Azure + .NET Core)
Frontend → Azure API Management (Gateway).
Commands → MediatR → Azure SQL.
Events → Azure Service Bus → Saga (Durable Functions).
Queries → Cosmos DB (denormalized).
Monitoring → Application Insights.
Deep Dive into CQRS with .NET Core and Azure Implementations
CQRS (Command Query Responsibility Segregation) is a powerful pattern for separating read and write operations in microservices. Below is a detailed breakdown of CQRS, covering core concepts, implementation strategies, and real-world Azure + .NET Core examples.
1. Core Concepts of CQRS
What is CQRS?
Commands (Writes): Modify state (e.g.,
CreateOrder,UpdateUser).Queries (Reads): Retrieve data (e.g.,
GetOrderById,ListUsers).Separate Models: Optimized for their purpose (e.g., normalized for writes, denormalized for reads).
Why Use CQRS?
✅ Scalability: Reads (often 80% of traffic) can scale independently.
✅ Performance: Optimized read models (e.g., materialized views).
✅ Flexibility: Different databases for reads vs. writes.
⚠ Complexity: Requires event syncing between models.
2. CQRS Implementation Strategies
A. Basic CQRS (Single Database)
Same database, but separate models (commands vs. queries).
Best for: Simple apps where eventual consistency is acceptable.
Example: MediatR in .NET Core
// Command (Write) public record CreateProductCommand(string Name, decimal Price) : IRequest<Guid>; public class CreateProductHandler : IRequestHandler<CreateProductCommand, Guid> { private readonly AppDbContext _db; public CreateProductHandler(AppDbContext db) => _db = db; public async Task<Guid> Handle(CreateProductCommand cmd, CancellationToken ct) { var product = new Product { Name = cmd.Name, Price = cmd.Price }; _db.Products.Add(product); await _db.SaveChangesAsync(ct); return product.Id; } } // Query (Read) public record GetProductByIdQuery(Guid Id) : IRequest<ProductDto>; public class GetProductByIdHandler : IRequestHandler<GetProductByIdQuery, ProductDto> { private readonly AppDbContext _db; public GetProductByIdHandler(AppDbContext db) => _db = db; public async Task<ProductDto> Handle(GetProductByIdQuery query, CancellationToken ct) => await _db.Products .Where(p => p.Id == query.Id) .Select(p => new ProductDto(p.Id, p.Name, p.Price)) .FirstOrDefaultAsync(ct); }
B. Advanced CQRS (Separate Databases)
Write DB (OLTP): Optimized for transactions (e.g., Azure SQL).
Read DB (OLAP): Optimized for queries (e.g., Cosmos DB, Redis Cache).
Syncing Data: Use events (e.g.,
ProductCreated) to keep models in sync.
Example: Event-Driven CQRS with Azure Service Bus
// 1. Command Side (Write DB - Azure SQL) public class ProductCommandHandler { private readonly AppDbContext _db; private readonly IEventPublisher _publisher; public ProductCommandHandler(AppDbContext db, IEventPublisher publisher) => (_db, _publisher) = (db, publisher); public async Task<Guid> Handle(CreateProductCommand cmd) { var product = new Product { Name = cmd.Name, Price = cmd.Price }; _db.Products.Add(product); await _db.SaveChangesAsync(); // Publish event to update read model await _publisher.Publish(new ProductCreatedEvent(product.Id, product.Name, product.Price)); return product.Id; } } // 2. Event Handler (Updates Read DB - Cosmos DB) public class ProductCreatedEventHandler { private readonly CosmosClient _cosmos; public ProductCreatedEventHandler(CosmosClient cosmos) => _cosmos = cosmos; public async Task Handle(ProductCreatedEvent @event) { var container = _cosmos.GetContainer("ProductsDB", "Products"); await container.UpsertItemAsync(new ProductReadModel { Id = @event.Id, Name = @event.Name, Price = @event.Price }); } }
Azure Services Used
| Component | Technology |
|---|---|
| Command Database | Azure SQL (EF Core) |
| Query Database | Cosmos DB (optimized for reads) |
| Event Bus | Azure Service Bus / Event Grid |
| Event Processor | Azure Functions (Serverless) |
3. Optimizing Read Models
A. Materialized Views
Pre-computed query results stored for fast access.
Example:
-- In Azure SQL (for reporting) CREATE VIEW ActiveUsers AS SELECT Id, Name FROM Users WHERE IsActive = 1;
B. Caching (Redis)
Cache frequent queries (e.g.,
GetTopProducts).Example: .NET Core + Azure Redis Cache
public class CachedProductQueryHandler { private readonly IDatabase _cache; private readonly AppDbContext _db; public CachedProductQueryHandler(IConnectionMultiplexer redis, AppDbContext db) => (_cache, _db) = (redis.GetDatabase(), db); public async Task<ProductDto> Handle(GetProductByIdQuery query) { var cacheKey = $"product:{query.Id}"; var cachedProduct = await _cache.StringGetAsync(cacheKey); if (cachedProduct.HasValue) return JsonSerializer.Deserialize<ProductDto>(cachedProduct!); var product = await _db.Products.FindAsync(query.Id); await _cache.StringSetAsync(cacheKey, JsonSerializer.Serialize(product), TimeSpan.FromMinutes(5)); return product; } }
4. When to Use CQRS?
| Scenario | Recommended Approach |
|---|---|
| Simple CRUD app | Basic CQRS (MediatR) |
| High-read systems (e.g., dashboards) | Separate read DB (Cosmos DB) |
| Real-time analytics | Event Sourcing + CQRS |
| Microservices with eventual consistency | Event-Driven CQRS |
5. Full CQRS Architecture (Azure + .NET Core)
flowchart LR Client -->|Commands| API[API Gateway] API -->|"CreateProduct"| Command[Command Handler] Command -->|"Save to SQL DB"| SQL[(Azure SQL)] Command -->|"Publish ProductCreated"| SB[Service Bus] SB -->|"Update Read Model"| Query[Query Handler] Query -->|"Save to Cosmos DB"| Cosmos[(Cosmos DB)] Client -->|"GetProduct"| API API -->|Query| Cosmos
6. Key Takeaways
Start simple (MediatR) before scaling to separate databases.
Use events (Service Bus/Event Grid) to sync read/write models.
Optimize reads with caching (Redis) or materialized views.
Azure Services:
Commands: Azure SQL + EF Core.
Queries: Cosmos DB + Redis Cache.
Events: Service Bus + Azure Functions.
Need a Working Example?
Check out this .NET CQRS Template:
🔗 GitHub: .NET CQRS with Azure
No comments:
Post a Comment