Back to: Microservices using ASP.NET Core Web API Tutorials
Implement the Saga Pattern in OrderService
The Saga Pattern in the OrderService maintains data consistency across multiple microservices during order processing. It ensures that when an order is placed, confirmed, or cancelled, all related services, such as Product, Payment, and Notification, stay in sync.
The OrderService starts the Saga by publishing events and also performs compensation when something fails in other services. Each class in this implementation has a clear role in publishing, consuming, or handling these events, ensuring a reliable, consistent process.
This implementation of the Saga Pattern within the OrderService focuses on two primary responsibilities:
- Triggering the Saga by publishing an OrderPlacedEvent after a successful order creation.
- Handling compensation by consuming an OrderCancelledEvent from the Orchestrator when a transaction in another service fails (e.g., stock unavailability or payment failure).
OrderService.Contracts
Messaging/IOrderPlacedEventPublisher.cs
This interface defines a contract for publishing the OrderPlacedEvent without exposing any RabbitMQ or infrastructure-level dependencies to the Application Layer. It allows the application to trigger the Saga process by sending events in a clean and testable way. So, create an interface named IOrderPlacedEventPublisher.cs within the Messaging folder of OrderService.Contracts project and copy-paste the following code. This will be implemented by the Infrastructure layer.
using Messaging.Common.Events;
namespace OrderService.Contracts.Messaging
{
// Layer: Contracts (shared between Application and Infrastructure)
// Purpose:
// Defines a contract for publishing the "OrderPlacedEvent" message.
// This event is raised by the OrderService when a new order is successfully created.
// It notifies the OrchestratorService to start the Saga workflow that manages
// coordination among other microservices (like ProductService, PaymentService,
// and NotificationService).
// Why Interface?
// Using an interface decouples the Application layer from the
// actual RabbitMQ implementation. The Application layer only depends
// on this abstraction, while the Infrastructure layer provides
// the concrete publishing logic.
public interface IOrderPlacedEventPublisher
{
// Method: PublishOrderPlacedAsync
// Description:
// Publishes the "OrderPlacedEvent" to the message broker (RabbitMQ).
// This marks the starting point of the Saga Orchestration flow.
// Once published, the OrchestratorService receives this event and
// coordinates the following actions:
// - Requests ProductService to reserve stock.
// Based on the outcome, the Orchestrator may publish
// either an OrderConfirmedEvent or an OrderCancelledEvent.
// Parameters:
// evt → The event payload containing order details such as
// OrderId, UserId, TotalAmount, and list of ordered items.
// Return Type:
// Task → Represents an asynchronous operation, ensuring that
// publishing can be awaited without blocking threads.
// Notes:
// - The Infrastructure layer (OrderPlacedEventPublisher class)
// implements this interface and handles RabbitMQ publishing.
// - The Application layer calls this method after the order
// is successfully saved in the database.
// - The OrchestratorService, not other microservices directly,
// receives this event first and controls the next steps
// in the Saga coordination process.
Task PublishOrderPlacedAsync(OrderPlacedEvent evt);
}
}
Uses:
- Keeps the Application Logic Independent of RabbitMQ implementation.
- Triggers the Saga workflow by publishing the event after order creation.
- Keeps Application Layer focused on business logic rather than messaging logic.
- Ensures that publishing logic can be changed later without affecting core code.
Messaging/IOrderCancelledHandler.cs
This interface defines the contract for how the Application Layer should handle compensation when an order is cancelled. It allows the Infrastructure Layer consumer to safely delegate business logic to the application. Create an interface named IOrderCancelledHandler.cs within the Messaging folder of OrderService.Contracts project and copy-paste the following code.
using Messaging.Common.Events;
namespace OrderService.Contracts.Messaging
{
// Layer: Contracts (shared between Application and Infrastructure)
// Purpose:
// Defines a contract for handling the "OrderCancelledEvent" message.
// When the Orchestrator publishes an order cancellation event (for example,
// due to stock unavailability), the OrderService must perform a
// compensation action such as updating the order status in its database.
// Why Interface?
// Using an interface ensures that the Application layer (business logic)
// implements the compensation behavior, while the Infrastructure layer
// simply triggers it — promoting clean separation of concerns.
public interface IOrderCancelledHandler
{
//HandleAsync Method
// Executes the compensation logic when an order cancellation event
// is received from RabbitMQ (published by the Orchestrator Service).
// The implementation of this method will update the order’s status
// (e.g., from "Pending" or "Confirmed" to "Cancelled") in the database.
// Parameters:
// message → The event payload (OrderCancelledEvent) that contains
// details like OrderId, Reason for cancellation, etc.
// Return Type:
// Task → Indicates that the operation is asynchronous.
// Notes:
// - This method will be implemented in the Application layer.
// - The Infrastructure layer (consumer) will call this method after
// deserializing the message from RabbitMQ.
Task HandleAsync(OrderCancelledEvent message);
}
}
Uses:
- Provides a clear separation between message consumption and business logic.
- Ensures the Infrastructure layer (consumer) only delegates work.
- Allows the Application layer to handle compensation logic (mark order as Cancelled).
- Supports dependency injection for database and logging services.
OrderService.Infrastructure
OrderPlacedEventPublisher.cs (Infrastructure Layer → Messaging/Producers)
Implements IOrderPlacedEventPublisher and handles event publication to RabbitMQ. It’s the first step in the Saga, notifying other services (such as Product or Notification) that a new order has been placed. Create a class file named OrderPlacedEventPublisher.cs within the Messaging/Producers folder of OrderService.Infrastructure project and copy-paste the following code.
using Messaging.Common.Events;
using Messaging.Common.Options;
using Messaging.Common.Publishing;
using Microsoft.Extensions.Options;
using OrderService.Contracts.Messaging;
namespace OrderService.Infrastructure.Messaging.Producers
{
// Purpose:
// Implements the IOrderPlacedEventPublisher interface.
// Responsible for publishing the "OrderPlacedEvent" message
// to RabbitMQ after a new order is successfully created.
// In the Orchestration-based Saga pattern, this event is the starting point.
// It is consumed by the OrchestratorService,
// which coordinates the overall workflow across microservices
// (ProductService, PaymentService, NotificationService, etc.).
public sealed class OrderPlacedEventPublisher : IOrderPlacedEventPublisher
{
// Field: _publisher
// Description:
// Shared publishing abstraction from Messaging.Common.
// Handles all low-level RabbitMQ publishing operations such as:
// - Message serialization (usually JSON)
// - Adding correlation IDs for distributed tracing
// - Ensuring message persistence and reliability
// Keeps the OrderService free from RabbitMQ-specific logic.
private readonly IPublisher _publisher;
// Field: _options
// Description:
// Strongly-typed configuration settings for RabbitMQ.
// Includes:
// - Exchange name (e.g., "ecommerce.topic")
// - Routing keys (e.g., "order.placed")
// - Queue names and connection details
// Centralized and configurable message routing without hardcoding values.
private readonly RabbitMqOptions _options;
// Constructor:
// Injects dependencies via .NET's built-in dependency injection (DI).
// Parameters:
// - publisher → IPublisher abstraction for publishing messages.
// - options → IOptions<RabbitMqOptions> to access RabbitMQ configuration.
public OrderPlacedEventPublisher(IPublisher publisher, IOptions<RabbitMqOptions> options)
{
_publisher = publisher;
_options = options.Value;
}
// Method: PublishOrderPlacedAsync
// Description:
// Publishes the "OrderPlacedEvent" to RabbitMQ.
// This event informs the OrchestratorService that a new order
// has been placed and that it should begin the Saga workflow.
// The OrchestratorService then:
// - Sends a "StockReservationRequestedEvent" to ProductService.
// Parameters:
// evt → The actual event data containing order details like
// OrderId, UserId, TotalAmount, and Item details.
// Return:
// Task → Represents the asynchronous publish operation.
public Task PublishOrderPlacedAsync(OrderPlacedEvent evt)
{
// Log (for development/debugging purposes only).
// Helps confirm that the event has been triggered successfully.
Console.WriteLine($"[Publish] OrderPlacedEvent sent for OrderId={evt.OrderId}");
// Publish the event using the shared IPublisher abstraction.
// - exchange: The central topic exchange shared across all services, e.g., "ecommerce.topic"
// - routingKey: Determines which queue(s) will receive the event.
// For this event, the key is typically "order.placed".
// - message: The actual event payload to be serialized and sent.
// This message will be consumed first by the OrchestratorService,
// which coordinates all subsequent microservice operations.
return _publisher.PublishAsync(
exchange: _options.ExchangeName, // e.g., "ecommerce.topic"
routingKey: _options.RkOrderPlaced, // e.g., "order.placed"
message: evt // event payload (OrderPlacedEvent)
);
}
}
}
Uses:
- Sends OrderPlacedEvent to RabbitMQ after order creation.
- Starts the Saga orchestration flow.
- Uses centralized RabbitMQOptions for consistent configuration.
- Supports distributed tracing via correlationId.
- Keeps publishing logic reusable and consistent across services.
OrderCancelledConsumer.cs (Infrastructure Layer → Messaging/Consumers)
Listens for OrderCancelledEvent messages from RabbitMQ and triggers compensation. It does not execute business logic directly; instead, it passes messages to the IOrderCancelledHandler in the Application Layer. Create a class file named OrderCancelledConsumer.cs within the Messaging/Consumers folder of OrderService.Infrastructure project and copy-paste the following code.
using Messaging.Common.Consuming;
using Messaging.Common.Events;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OrderService.Contracts.Messaging;
using RabbitMQ.Client;
namespace OrderService.Infrastructure.Messaging.Consumers
{
// Purpose:
// Listens for "OrderCancelledEvent" messages published by the
// OrchestratorService during the Saga compensation process.
// When a downstream service (like ProductService or PaymentService) fails,
// the OrchestratorService decides to cancel the order and publishes an OrderCancelledEvent.
// This consumer receives that message and triggers compensation logic in the OrderService
// (e.g., updating the order’s status in the database to "Cancelled").
// Notes:
// - Inherits from BaseConsumer<T>, which provides core message
// consumption and deserialization behavior.
// - Uses dependency injection to create scopes and resolve
// services safely (e.g., DbContext via Application layer handler).
public sealed class OrderCancelledConsumer : BaseConsumer<OrderCancelledEvent>
{
// Field: _scopeFactory
// Description:
// The IServiceScopeFactory is used to create a new DI scope for every incoming message.
// Since this consumer runs as a BackgroundService (singleton),
// we cannot inject scoped services (like DbContext) directly.
private readonly IServiceScopeFactory _scopeFactory;
// Constructor:
// Parameters:
// - channel: The RabbitMQ channel (IModel) used to subscribe
// to the appropriate queue and consume messages.
// - queueName: The specific queue name this consumer listens on,
// typically configured in RabbitMqOptions (e.g.,
// "order.compensation_cancelled").
// - scopeFactory: Used to create per-message DI scopes for resolving
// scoped services like repositories or DbContexts.
// - logger: Used to record information or errors during message
// processing for observability and debugging.
// Notes:
// - The base class (BaseConsumer<OrderCancelledEvent>) handles
// low-level RabbitMQ consumer setup and message acknowledgment.
public OrderCancelledConsumer(
IModel channel,
string queueName,
IServiceScopeFactory scopeFactory,
ILogger<OrderCancelledConsumer> logger)
: base(channel, queueName, logger) // Calls BaseConsumer constructor
{
_scopeFactory = scopeFactory;
}
// Method: HandleMessage
// Description:
// Executes when a new "OrderCancelledEvent" message is received from RabbitMQ.
// This method delegates the actual business logic
// (updating the order status to Cancelled) to the Application layer.
// Flow:
// 1. Create a new DI scope for safe usage of scoped services.
// 2. Resolve IOrderCancelledHandler from the scoped service provider.
// 3. Pass the deserialized event and correlationId to the handler.
// 4. Handler updates the database (order → Cancelled).
// Parameters:
// - message: The OrderCancelledEvent payload received from the queue.
protected override async Task HandleMessage(OrderCancelledEvent message)
{
// Create a new dependency injection scope for this message.
// This ensures that scoped dependencies (like DbContext or repositories)
// are properly managed and disposed after the message is processed.
using var scope = _scopeFactory.CreateScope();
// Resolve the application-layer handler responsible for executing
// the compensation logic (marking the order as Cancelled).
var handler = scope.ServiceProvider.GetRequiredService<IOrderCancelledHandler>();
// Invoke the handler method asynchronously.
// This is where the business logic (database update, audit log, etc.)
// is executed to finalize the compensation.
await handler.HandleAsync(message);
}
}
}
Uses:
- Listens to Saga compensation messages (order.cancelled).
- Delegates business logic to the application layer safely via DI scope.
- Manages message deserialization, ACK/NACK, and logging.
- Ensures that scoped services (such as DbContext) are handled correctly.
Messaging/Extensions/RabbitMqConsumerExtensions
This extension method simplifies registering RabbitMQ consumers in the DI container. It adds OrderCancelledConsumer as a hosted background service cleanly and makes the Program.cs is more readable. Create a class file named RabbitMqConsumerExtensions.cs within the Messaging/Extensions folder of OrderService.Infrastructure project and copy-paste the following code.
using Messaging.Common.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OrderService.Infrastructure.Messaging.Consumers;
using RabbitMQ.Client;
namespace OrderService.Infrastructure.Messaging.Extensions
{
// Purpose:
// Provides an extension method for registering the
// OrderCancelledConsumer as a hosted background service.
//
// This helps modularize RabbitMQ consumer registration logic
// and keeps Program.cs clean and maintainable.
//
// In the Saga Orchestration flow:
// - The OrchestratorService publishes "OrderCancelledEvent"
// when a distributed transaction fails (e.g., stock unavailable).
// - This consumer listens for that message and triggers
// compensation logic inside OrderService.
public static class RabbitMqConsumerExtensions
{
// Method: AddOrderCancelledConsumer
// Description:
// Registers the OrderCancelledConsumer as a Hosted Service.
// Hosted services run continuously in the background and are
// ideal for long-running message listeners like RabbitMQ consumers.
//
// Parameters:
// services → IServiceCollection used for dependency injection.
//
// Return:
// IServiceCollection → Allows method chaining after registration.
//
// Notes:
// - This pattern helps you easily add more consumers in the future
// (e.g., AddOrderConfirmedConsumer, AddPaymentFailedConsumer, etc.)
// - Keeps Program.cs less cluttered and more modular.
public static IServiceCollection AddOrderCancelledConsumer(this IServiceCollection services)
{
// Register the OrderCancelledConsumer as a background service.
// The AddHostedService method automatically manages its lifecycle:
// - Starts when the application starts.
// - Stops when the app shuts down.
services.AddHostedService(sp =>
{
// Resolve all required dependencies from the DI container.
// RabbitMQ channel (IModel) used for queue subscription and message consumption.
var channel = sp.GetRequiredService<IModel>();
// Scope factory used to create DI scopes for resolving scoped services
// (like DbContext) inside the consumer while handling each message.
var scopeFactory = sp.GetRequiredService<IServiceScopeFactory>();
// Logger for the OrderCancelledConsumer to log message processing info and errors.
var logger = sp.GetRequiredService<ILogger<OrderCancelledConsumer>>();
// Retrieve RabbitMQ configuration (exchange, queues, routing keys) from appsettings.json.
var options = sp.GetRequiredService<IOptions<RabbitMqOptions>>().Value;
// Create and return an instance of OrderCancelledConsumer.
// Parameters:
// - channel: shared RabbitMQ communication channel.
// - options.QOrderCompensationCancelled: queue name for
// compensation messages ("order.compensation_cancelled").
// - scopeFactory: used to create new scopes for message processing.
// - logger: logs message handling operations.
//
// The consumer automatically subscribes to the queue when started.
return new OrderCancelledConsumer(
channel,
options.QOrderCompensationCancelled, // Queue name from RabbitMqOptions
scopeFactory,
logger
);
});
// Return IServiceCollection for method chaining.
return services;
}
}
}
Uses:
- Simplifies registration of RabbitMQ consumers.
- Injects dependencies such as IModel, IServiceScopeFactory, and RabbitMqOptions.
- Pulls the queue name (QOrderCompensationCancelled) from the configuration.
- Ensures consumer auto-start as a Hosted Service.
- Keeps Program.cs clean and modular.
- Follows the open–closed principle (easy to add more consumers later).
OrderCancelledHandler.cs (Application Layer → Messaging)
Implements the compensation logic for the Saga. When an order cancellation event arrives (due to failed stock reservation or other issues), it updates the database and logs the outcome. Create a class file named OrderCancelledHandler.cs within the Messaging folder of OrderService.ApplicaionLayer project and copy-paste the following code.
using Messaging.Common.Events;
using Microsoft.Extensions.Logging;
using OrderService.Contracts.Messaging;
using OrderService.Domain.Enums;
using OrderService.Domain.Repositories;
namespace OrderService.Application.Messaging
{
// Purpose:
// Implements the IOrderCancelledHandler interface to execute
// the compensation logic in the Saga Orchestration flow.
//
// When the OrchestratorService determines that an order cannot
// be completed (e.g., stock reservation or payment failure),
// it publishes an "OrderCancelledEvent".
//
// The OrderCancelledConsumer (in Infrastructure layer) receives
// this message and delegates the work to this handler.
//
// This handler performs the actual business operation by marking
// the order as "Cancelled" in the database and logging the outcome.
//
// Design Notes:
// - The Application layer is responsible for business logic.
// - The Infrastructure layer only handles message delivery.
public sealed class OrderCancelledHandler : IOrderCancelledHandler
{
// Field: _orderRepository
// Provides data access methods for updating order records
// in the database. Used here to change the order status to "Cancelled".
private readonly IOrderRepository _orderRepository;
// Field: _logger
// Used to log key actions and results during compensation.
// Helps trace when and why an order was cancelled.
private readonly ILogger<OrderCancelledHandler> _logger;
// Constructor:
// Parameters:
// - orderRepository → Repository abstraction to perform database operations.
// - logger → Logger used for tracking execution and errors.
public OrderCancelledHandler(
IOrderRepository orderRepository,
ILogger<OrderCancelledHandler> logger)
{
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
// Method: HandleAsync
// Description:
// Executes the compensation logic when an OrderCancelledEvent
// is received from the OrchestratorService.
//
// This method updates the order status in the database to "Cancelled"
// and logs important details for traceability and debugging.
//
// Flow:
// 1. Receive cancellation event from Orchestrator (via consumer).
// 2. Log the start of the compensation process with OrderId and reason.
// 3. Update the order’s status in the database (Cancelled).
// 4. Log the successful completion or throw exception if failed.
//
// Parameters:
// - message → The event data received (OrderId, Reason, etc.).
// - correlationId → Unique ID to trace this cancellation across
// multiple services in the Saga transaction.
//
// Return Type:
// Task → Asynchronous operation for non-blocking DB update.
//
// Notes:
// - If the DB update fails, the exception ensures visibility in logs.
// - RabbitMQ will handle retries or move the message to a dead-letter queue.
public async Task HandleAsync(OrderCancelledEvent message)
{
// Log the start of the compensation process.
// Includes correlationId for end-to-end Saga tracing.
_logger.LogInformation($"Starting compensation for OrderId: {message.OrderId}, Reason: { message.Reason}");
// Attempt to change the order’s status in the database to “Cancelled”.
// This is the key compensation step that ensures consistency
// after a distributed transaction failure.
var ok = await _orderRepository.ChangeOrderStatusAsync(
message.OrderId,
OrderStatusEnum.Cancelled, // Set new status
changedBy: "Orchestrator", // For audit tracking: who initiated the change
remarks: message.Reason); // Reason provided by Orchestrator (e.g., "Stock unavailable")
// Check if the update was successful.
// If not, throw an exception so the failure is logged and retried.
if (!ok)
{
throw new InvalidOperationException(
$"Failed to update order {message.OrderId} status to Cancelled.");
}
// Log successful completion of compensation.
// Confirms that the order has been properly marked as Cancelled.
_logger.LogInformation(
"Order compensation successful for OrderId: {OrderId}", message.OrderId);
}
}
}
Uses:
- Updates order status to “Cancelled” in the database.
- Logs details for audit and monitoring.
- Handles failures by throwing exceptions (for retries or DLX).
- Ensures the Saga compensation step is reliable and traceable.
- Keeps domain-specific logic in the Application Layer (not Infrastructure).
OrderService.Application
Acts as the main service handling order creation, confirmation, and integration with other microservices. It triggers the Saga by publishing events after successful order creation or payment confirmation.
Injecting IOrderPlacedEventPublisher in OrderService
public class OrderService : IOrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IUserServiceClient _userServiceClient;
private readonly IProductServiceClient _productServiceClient;
private readonly IPaymentServiceClient _paymentServiceClient;
private readonly INotificationServiceClient _notificationServiceClient;
private readonly IMapper _mapper;
private readonly IMasterDataRepository _masterDataRepository;
private readonly IConfiguration _configuration;
private readonly IOrderPlacedEventPublisher _publisher;
public OrderService(
IOrderRepository orderRepository,
IUserServiceClient userServiceClient,
IProductServiceClient productServiceClient,
IPaymentServiceClient paymentServiceClient,
INotificationServiceClient notificationServiceClient,
IMasterDataRepository masterDataRepository,
IMapper mapper,
IConfiguration configuration,
IOrderPlacedEventPublisher publisher)
{
// Initialize dependencies with null checks for safe injection
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_userServiceClient = userServiceClient ?? throw new ArgumentNullException(nameof(userServiceClient));
_productServiceClient = productServiceClient ?? throw new ArgumentNullException(nameof(productServiceClient));
_paymentServiceClient = paymentServiceClient ?? throw new ArgumentNullException(nameof(paymentServiceClient));
_notificationServiceClient = notificationServiceClient ?? throw new ArgumentNullException(nameof(notificationServiceClient));
_masterDataRepository = masterDataRepository ?? throw new ArgumentNullException(nameof(masterDataRepository));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_publisher = publisher;
}
//Existing Code
}
CreateOrderAsync
The most important change here is using order.Id.ToString() as CorrelationId, which ensures end-to-end tracing of the order across multiple services. Please modify the CreateOrderAsync method of the OrderService.cs class file as follows:
public async Task<OrderResponseDTO> CreateOrderAsync(CreateOrderRequestDTO request, string accessToken)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (request.Items == null || !request.Items.Any())
throw new ArgumentException("Order must have at least one item.");
// Validate that the user exists via User Microservice
var user = await _userServiceClient.GetUserByIdAsync(request.UserId, accessToken);
if (user == null)
throw new InvalidOperationException("User does not exist.");
// Resolve Shipping Address ID, either provided or created newly via User Microservice
Guid? shippingAddressId = null;
if (request.ShippingAddressId != null)
{
shippingAddressId = request.ShippingAddressId;
}
else if (request.ShippingAddress != null)
{
request.ShippingAddress.UserId = request.UserId;
shippingAddressId = await _userServiceClient.SaveOrUpdateAddressAsync(request.ShippingAddress, accessToken);
}
// Resolve Billing Address ID, either provided or created newly
Guid? billingAddressId = null;
if (request.BillingAddressId != null)
{
billingAddressId = request.BillingAddressId;
}
else if (request.BillingAddress != null)
{
request.BillingAddress.UserId = request.UserId;
billingAddressId = await _userServiceClient.SaveOrUpdateAddressAsync(request.BillingAddress, accessToken);
}
// Validate presence of both addresses
if (shippingAddressId == null || billingAddressId == null)
throw new ArgumentException("Both ShippingAddressId and BillingAddressId must be provided or created.");
// Validate product stock availability but do not reduce stock yet
var stockCheckRequests = request.Items.Select(i => new ProductStockVerificationRequestDTO
{
ProductId = i.ProductId,
Quantity = i.Quantity
}).ToList();
var stockValidation = await _productServiceClient.CheckProductsAvailabilityAsync(stockCheckRequests, accessToken);
if (stockValidation == null || stockValidation.Any(x => !x.IsValidProduct || !x.IsQuantityAvailable))
throw new InvalidOperationException("One or more products are invalid or out of stock.");
// Retrieve latest product info for accurate pricing and discount
var productIds = request.Items.Select(i => i.ProductId).ToList();
var products = await _productServiceClient.GetProductsByIdsAsync(productIds, accessToken);
if (products == null || products.Count != productIds.Count)
throw new InvalidOperationException("Failed to retrieve product details for all items.");
try
{
// Fetch policies (example placeholders, adjust with your actual logic)
int? cancellationPolicyId = null;
int? returnPolicyId = null;
// Example: fetch cancellation policy based on user or other criteria
var cancellationPolicy = await _masterDataRepository.GetActiveCancellationPolicyAsync();
if (cancellationPolicy != null)
cancellationPolicyId = cancellationPolicy.Id;
var returnPolicy = await _masterDataRepository.GetActiveReturnPolicyAsync();
if (returnPolicy != null)
returnPolicyId = returnPolicy.Id;
var orderId = Guid.NewGuid();
var orderNumber = GenerateOrderNumberFromGuid(orderId);
var now = DateTime.UtcNow;
var initialStatus = request.PaymentMethod == PaymentMethodEnum.COD
? OrderStatusEnum.Confirmed // COD orders confirmed immediately
: OrderStatusEnum.Pending; // Online payment orders start as pending
// Create order entity
var order = new Order
{
Id = orderId,
OrderNumber = orderNumber,
UserId = request.UserId,
ShippingAddressId = shippingAddressId.Value,
BillingAddressId = billingAddressId.Value,
PaymentMethod = request.PaymentMethod.ToString(),
OrderStatusId = (int)initialStatus,
CreatedAt = now,
OrderDate = now,
CancellationPolicyId = cancellationPolicyId,
ReturnPolicyId = returnPolicyId,
OrderItems = new List<OrderItem>()
};
// Add order items with fresh product data
foreach (var item in request.Items)
{
var product = products.First(p => p.Id == item.ProductId);
order.OrderItems.Add(new OrderItem
{
Id = Guid.NewGuid(),
OrderId = order.Id,
ProductId = product.Id,
ProductName = product.Name,
PriceAtPurchase = product.Price,
DiscountedPrice = product.DiscountedPrice,
Quantity = item.Quantity,
ItemStatusId = (int)initialStatus
});
}
// Calculate order totals: subtotal, discount, tax, shipping, and final amount
order.SubTotalAmount = Math.Round(order.OrderItems.Sum(i => i.PriceAtPurchase * i.Quantity), 2, MidpointRounding.AwayFromZero);
order.DiscountAmount = Math.Round(await CalculateDiscountAmountAsync(order.OrderItems), 2, MidpointRounding.AwayFromZero);
order.TaxAmount = Math.Round(await CalculateTaxAmountAsync(order.SubTotalAmount - order.DiscountAmount), 2, MidpointRounding.AwayFromZero);
order.ShippingCharges = Math.Round(CalculateShippingCharges(order.SubTotalAmount - order.DiscountAmount), 2, MidpointRounding.AwayFromZero);
order.TotalAmount = Math.Round(order.SubTotalAmount - order.DiscountAmount + order.TaxAmount + order.ShippingCharges, 2, MidpointRounding.AwayFromZero);
// Save order to repository
var addedOrder = await _orderRepository.AddAsync(order);
if (addedOrder == null)
throw new InvalidOperationException("Failed to create order.");
// Initiate payment via Payment Service
var paymentRequest = new CreatePaymentRequestDTO
{
OrderId = order.Id,
UserId = order.UserId,
Amount = order.TotalAmount,
PaymentMethod = request.PaymentMethod
};
var paymentResponse = await _paymentServiceClient.InitiatePaymentAsync(paymentRequest, accessToken);
if (paymentResponse == null)
throw new InvalidOperationException("Payment initiation failed.");
// For COD, immediately reserve stock and send notification
if (request.PaymentMethod == PaymentMethodEnum.COD)
{
#region Event Publishing to RabbitMQ
// Construct the integration event (OrderPlacedEvent)
var orderPlacedEvent = new OrderPlacedEvent
{
OrderId = order.Id,
OrderNumber = order.OrderNumber,
UserId = order.UserId,
CustomerName = user.FullName,
CustomerEmail = user.Email,
PhoneNumber = user.PhoneNumber,
TotalAmount = order.TotalAmount,
CorrelationId = order.Id.ToString(),
Items = order.OrderItems.Select(i => new OrderLineItem
{
ProductId = i.ProductId,
Quantity = i.Quantity,
UnitPrice = i.PriceAtPurchase
}).ToList()
};
// Publish the event to RabbitMQ using the shared publisher.
// This message will be routed to:
// - ProductService (to decrease stock)
// - NotificationService (to insert notification record).
// - Use OrderId as CorrelationId for traceability
await _publisher.PublishOrderPlacedAsync(orderPlacedEvent);
#endregion
// Map and return order DTO with confirmed status and no payment URL
var orderDto = _mapper.Map<OrderResponseDTO>(order);
orderDto.OrderStatus = OrderStatusEnum.Confirmed;
orderDto.PaymentMethod = PaymentMethodEnum.COD;
orderDto.PaymentUrl = null;
return orderDto;
}
else
{
// Map and return order DTO with pending status and payment URL
var orderDto = _mapper.Map<OrderResponseDTO>(order);
orderDto.OrderStatus = OrderStatusEnum.Pending;
orderDto.PaymentMethod = request.PaymentMethod;
orderDto.PaymentUrl = paymentResponse.PaymentUrl;
return orderDto;
}
}
catch (Exception ex)
{
//Log the Exception
Console.WriteLine(ex.Message);
throw;
}
}
ConfirmOrderAsync
Please modify the ConfirmOrderAsync method of the OrderService.cs class file as follows:
public async Task<bool> ConfirmOrderAsync(Guid orderId, string accessToken)
{
// Retrieve order
var order = await _orderRepository.GetByIdAsync(orderId);
if (order == null)
throw new KeyNotFoundException("Order not found.");
// Only allow confirmation if order is pending
if (order.OrderStatusId != (int)OrderStatusEnum.Pending)
throw new InvalidOperationException("Order is not in a pending state.");
// Retrieve payment info from Payment Service
var paymentInfo = await _paymentServiceClient.GetPaymentInfoAsync(
new PaymentInfoRequestDTO { OrderId = orderId }, accessToken);
if (paymentInfo == null)
throw new InvalidOperationException("Payment information not found for this order.");
if (paymentInfo.PaymentStatus != PaymentStatusEnum.Completed)
throw new InvalidOperationException("Payment is not successful.");
var user = await _userServiceClient.GetUserByIdAsync(order.UserId, accessToken);
if (user == null)
throw new InvalidOperationException("User does not exist.");
try
{
// Change order status to Confirmed
bool statusChanged = await _orderRepository.ChangeOrderStatusAsync(
orderId, OrderStatusEnum.Confirmed, "PaymentService", "Payment successful, order confirmed.");
if (!statusChanged)
throw new InvalidOperationException("Failed to update order status.");
// Now that the order is confirmed, publish Order Placed event
// Create the event payload that downstream services need
var orderPlacedEvent = new OrderPlacedEvent
{
OrderId = order.Id,
UserId = order.UserId,
CustomerName = user.FullName,
CustomerEmail = user.Email,
PhoneNumber = user.PhoneNumber,
TotalAmount = order.TotalAmount,
CorrelationId = order.Id.ToString(),
Items = order.OrderItems.Select(i => new OrderLineItem
{
ProductId = i.ProductId,
Quantity = i.Quantity,
UnitPrice = i.PriceAtPurchase
}).ToList()
};
// Publish the event to RabbitMQ
// - _publisher abstracts RabbitMQ communication
// - The message is sent to exchange "ecommerce.topic" with routing key "order.placed"
// - ProductService will consume this event to reduce stock
// - NotificationService will consume this event to insert a notification
await _publisher.PublishOrderPlacedAsync(orderPlacedEvent);
return true;
}
catch (Exception ex)
{
//Log the Exception
Console.WriteLine(ex.Message);
throw;
}
}
Program Class File:
The startup configuration file that wires together all dependencies, messaging components, and application settings. It sets up RabbitMQ topology and registers all required services for the Order microservice to participate in the Saga. Please modify the Program class file as follows:
using Messaging.Common.Extensions;
using Messaging.Common.Options;
using Messaging.Common.Publishing;
using Messaging.Common.Topology;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OrderService.Application.Interfaces;
using OrderService.Application.MappingProfiles;
using OrderService.Application.Mappings;
using OrderService.Application.Messaging;
using OrderService.Application.Services;
using OrderService.Contracts.Messaging;
using OrderService.Infrastructure.DependencyInjection;
using OrderService.Infrastructure.Messaging.Extensions;
using OrderService.Infrastructure.Messaging.Producers;
using OrderService.Infrastructure.Persistence;
using RabbitMQ.Client;
using System.Text;
using System.Text.Json.Serialization;
namespace OrderService.API
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddInfrastructureServices(builder.Configuration);
// Add DbContext
builder.Services.AddDbContext<OrderDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Register services
builder.Services.AddScoped<ICartService, CartService>();
builder.Services.AddScoped<IOrderService, OrderService.Application.Services.OrderService>();
builder.Services.AddScoped<ICancellationService, CancellationService>();
builder.Services.AddScoped<IReturnService, ReturnService>();
builder.Services.AddScoped<IRefundService, RefundService>();
builder.Services.AddScoped<IShipmentService, ShipmentService>();
// Add AutoMapper Mapping Profiles
builder.Services.AddAutoMapper(typeof(CartMappingProfile));
builder.Services.AddAutoMapper(typeof(OrderMappingProfile));
builder.Services.AddAutoMapper(typeof(CancellationMappingProfile));
builder.Services.AddAutoMapper(typeof(ReturnMappingProfile));
builder.Services.AddAutoMapper(typeof(RefundMappingProfile));
builder.Services.AddAutoMapper(typeof(ShipmentMappingProfile));
//Adding JWT Authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["JwtSettings:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtSettings:SecretKey"]!))
};
});
// ------------------------------------------------------------
// 1️. Load RabbitMQ configuration options
// ------------------------------------------------------------
// Reads all RabbitMQ settings from appsettings.json.
// These settings are bound to the strongly - typed RabbitMqOptions class.
// - The first line registers the configuration section with DI
// so it can be injected using IOptions<RabbitMqOptions>.
// - The second line retrieves the same options immediately for use below.
builder.Services.Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq"));
var mq = builder.Configuration.GetSection("RabbitMq").Get<RabbitMqOptions>()!;
// ------------------------------------------------------------
// 2️. Register RabbitMQ Connection and Channel
// ------------------------------------------------------------
// - AddRabbitMq() is an extension method from Messaging.Common.Extensions.
// - It internally:
// * Creates a single RabbitMQ connection using the provided host credentials.
// * Opens a shared channel (IModel) used by both publishers and consumers.
// * Registers these as singletons in the DI container.
// - This avoids creating multiple connections/channels unnecessarily.
// - The shared channel improves performance and stability across publishers and consumers.
builder.Services.AddRabbitMq(mq.HostName, mq.UserName, mq.Password, mq.VirtualHost);
// ------------------------------------------------------------
// 3️. Register Core Publisher
// ------------------------------------------------------------
// - IPublisher is an abstraction that defines a simple method to publish messages
// (PublishAsync(exchange, routingKey, message, correlationId)).
// - Publisher is the concrete implementation that uses RabbitMQ under the hood.
builder.Services.AddSingleton<IPublisher, Publisher>();
// ------------------------------------------------------------
// 4️. Register Domain-Specific Event Publisher
// ------------------------------------------------------------
// - IOrderPlacedEventPublisher is a domain-level abstraction for publishing
// the "OrderPlacedEvent" specifically from the OrderService.
// - OrderPlacedEventPublisher implements this interface and internally calls
// IPublisher.PublishAsync() using the exchange and routing key defined
// in RabbitMqOptions (e.g., "ecommerce.topic" + "order.placed").
builder.Services.AddSingleton<IOrderPlacedEventPublisher, OrderPlacedEventPublisher>();
// ------------------------------------------------------------
// 5️. Register Compensation Handler
// ------------------------------------------------------------
// - IOrderCancelledHandler is implemented by OrderCancelledHandler.
// - This handler defines what to do when the OrchestratorService
// sends an "OrderCancelledEvent" (e.g., due to stock or payment failure).
// - It updates the order status in the database to "Cancelled"
// — this is the Saga compensation step for OrderService.
builder.Services.AddScoped<IOrderCancelledHandler, OrderCancelledHandler>();
// ------------------------------------------------------------
// 6️. Register RabbitMQ Consumer for Compensation Messages
// ------------------------------------------------------------
// - The AddOrderCancelledConsumer() extension method (from Infrastructure.Messaging)
// registers the OrderCancelledConsumer as a background service.
// - The consumer:
// * Listens to the queue defined by QOrderCompensationCancelled (e.g., "order.compensation_cancelled").
// * Consumes OrderCancelledEvent messages published by OrchestratorService.
// * Invokes IOrderCancelledHandler in the Application layer for compensation logic.
// - HostedService ensures it runs continuously in the background and
// automatically reconnects if RabbitMQ restarts.
builder.Services.AddOrderCancelledConsumer();
var app = builder.Build();
// ------------------------------------------------------------
// 7. RabbitMQ Topology Bootstrap (One-time setup at startup)
// ------------------------------------------------------------
// - RabbitTopology.EnsureAll() declares exchanges, queues, and bindings
// if they do not already exist in RabbitMQ.
//
// Why Important?
// - Prevents runtime errors caused by missing queues or bindings.
// - Ensures consistent RabbitMQ setup across environments (dev/test/prod).
//
// What it does:
// - Declares the topic exchange (e.g., "ecommerce.topic").
// - Declares all required queues (like "order.placed", "order.compensation_cancelled").
// - Binds each queue to the correct routing key.
//
// This runs inside a scoped service block to safely access IModel and options.
using (var scope = app.Services.CreateScope())
{
var channel = scope.ServiceProvider.GetRequiredService<IModel>();
var opttions = scope.ServiceProvider.GetRequiredService<IOptions<RabbitMqOptions>>().Value;
RabbitTopology.EnsureAll(channel, opttions);
}
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
Uses:
- Configures RabbitMQ connection and exchange/queue bindings.
- Registers all publishers, consumers, and handlers in DI.
- Ensures RabbitTopology.EnsureAll() declares queues and exchanges.
- Loads RabbitMQ settings from appsettings.json.
Appsettings.json file:
Holds environment-specific configuration values for database connections, external services, JWT, and RabbitMQ. It allows centralized and environment-specific configuration. Please update the appsettings.json file as follows:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=OrderServiceDB;Trusted_Connection=True;TrustServerCertificate=True;"
},
"ExternalServices": {
"UserServiceUrl": "https://localhost:7269/",
"ProductServiceUrl": "https://localhost:7234",
"PaymentServiceUrl": "https://localhost:7154/",
"NotificationServiceUrl": "https://localhost:7192/"
},
"ShippingConfig": {
"IsShippingChargeAllowed": true,
"FreeShippingThreshold": 1000.00,
"ShippingCharge": 100
},
"JwtSettings": {
"Issuer": "UserService.API",
"SecretKey": "fPXxcJw8TW5sA+S4rl4tIPcKk+oXAqoRBo+1s2yjUS4="
},
"RabbitMq": {
"HostName": "localhost",
"Port": 5672,
"UserName": "ecommerce_user",
"Password": "Test@1234",
"VirtualHost": "ecommerce_vhost"
}
}
Uses:
- Defines RabbitMQ connection parameters (HostName, UserName, Password).
- Provides a database connection for OrderService.
- Configures external microservice URLs (User, Product, Payment, Notification).
- Supplies JWT Issuer and SecretKey for authentication.
- Enables environment-based configuration without code changes.
The Saga Pattern helps the OrderService handle distributed transactions smoothly without losing data consistency. It uses events to coordinate between services and performs compensation when needed. This design makes the system more reliable, easier to maintain, and ensures that all microservices work together correctly even when failures occur.

