Back to: Microservices using ASP.NET Core Web API Tutorials
Implementing Saga Pattern in the Product Service
The Product Service in the Saga Pattern acts as the Executor of Inventory Validation and Reservation. Once the OrchestratorService sends a StockReservationRequestedEvent, the Product Service checks whether enough stock is available for each product in the order. Depending on the result, it either publishes a StockReservedCompletedEvent (success path) or a StockReservationFailedEvent (compensation path).
This ensures that inventory-related decisions are handled locally within the Product Service’s domain while keeping the Saga workflow consistent across all microservices.
ProductService.Contracts
This layer defines the contracts (interfaces and models) that other layers (Application and Infrastructure) depend on. First, create two folders named Models and Messaging at the root of the ProductService.Contracts project.
Models/StockReservationResult.cs
Defines the result structure returned by the stock reservation process. It encapsulates:
- Success → Indicates whether the stock was reserved successfully.
- FailureReason → Explains why the reservation failed.
- FailedItems → A list of specific products that couldn’t be reserved (with quantity and reason).
This model standardizes how the system communicates stock validation results back to the Orchestrator, ensuring uniform handling of both success and failure scenarios. So, create a class file named StockReservationResult.cs within the Models folder, and copy-paste the following code.
using Messaging.Common.Models;
namespace ProductService.Contracts.Models
{
// Represents the result of a stock reservation attempt.
// This class is used to communicate back to the Orchestrator whether
// the stock reservation was successful or failed — and why.
public sealed class StockReservationResult
{
// Indicates whether the stock reservation succeeded for all requested items.
// If true → all items were available and reserved successfully.
// If false → one or more items could not be reserved.
public bool Success { get; set; }
// Provides a high-level reason for failure (e.g., "Insufficient stock").
// This is mainly for logging, debugging, or customer-facing notifications.
// Will be null or empty when Success = true.
public string? FailureReason { get; set; }
// A detailed list of items that failed to reserve.
// Each entry specifies which product failed, how many were requested,
// how many were available, and the failure reason.
// Empty when all items succeed.
public List<FailedLineItem> FailedItems { get; set; } = new();
}
}
Messaging/IStockReserveEventPublisher.cs
An interface declaring the StockReserveAsync() method, which performs inventory validation and updates. It accepts a StockReservationRequestedEvent and returns a StockReservationResult. So, create an interface named IStockReserveEventPublisher.cs within the Messaging folder, and copy-paste the following code.
using Messaging.Common.Events;
namespace ProductService.Contracts.Messaging
{
// Defines the contract for publishing stock reservation–related events
// from the ProductService to the message broker (RabbitMQ).
//
// This interface abstracts away the message publishing logic so that
// the ProductService can focus on business decisions (reserve/fail)
// instead of message transport details.
public interface IStockReserveEventPublisher
{
// Publishes a StockReservedCompletedEvent to RabbitMQ
// when the ProductService successfully reserves stock for all requested items.
//
// This event is consumed by the OrchestratorService, which then
// continues the Saga flow by confirming the order (publishes OrderConfirmedEvent).
Task PublishStockReservedCompletedAsync(StockReservedCompletedEvent evt);
// Publishes a StockReservationFailedEvent to RabbitMQ
// when the ProductService cannot reserve stock (e.g., insufficient quantity).
//
// This event signals the OrchestratorService to trigger compensation logic
// publishes OrderCancelledEvent to inform OrderService and NotificationService.
Task PublishStockReservationFailedAsync(StockReservationFailedEvent evt);
}
}
Messaging/IStockReserveService.cs
Abstracts the logic for publishing messages after stock reservation is processed. It contains:
- PublishStockReservedAsync() → Publishes success events.
- PublishStockReservationFailedAsync() → Publishes failure events.
Encapsulates RabbitMQ details and ensures all event publications follow consistent exchange and routing configurations. So, create an interface named IStockReserveService.cs within the Messaging folder, and copy-paste the following code.
using Messaging.Common.Events;
using ProductService.Contracts.Models;
namespace ProductService.Contracts.Messaging
{
// Defines the contract for stock reservation logic within the ProductService.
public interface IStockReserveService
{
// Validates and reserves stock for the given order request.
// This method is called when the OrchestratorService sends a
// StockReservationRequestedEvent asking the ProductService
// to verify and hold stock for all products in an order.
// Business Flow:
// 1. Check inventory for each product ID and requested quantity.
// 2. If all items are available → deduct stock and mark Success = true.
// 3. If any item is out of stock → mark Success = false and return failed items.
// 4. The result (success or failure) is then published back to RabbitMQ.
Task<StockReservationResult> StockReserveAsync(StockReservationRequestedEvent request);
}
}
ProductService.Application
This layer contains the core business logic for handling stock reservations. First, create a folder named Messaging at the root directory of ProductService.Application project.
Messaging/StockReserveService.cs
Implements the actual stock reservation logic.
Flow of Logic:
- Validate the incoming StockReservationRequestedEvent.
- Retrieve the requested product list from the database using IInventoryRepository.
- Check whether each product has sufficient stock.
- If any item fails:
-
- Return StockReservationResult with failed items and reasons.
-
- If all items pass:
-
- Decrease stock quantities in a transactional way.
- Return StockReservationResult with Success = true.
-
This class is the Core Business Engine of the ProductService. It ensures that inventory consistency is maintained before the order is confirmed in the Saga process. So, create a class file named StockReserveService.cs within the Messaging folder, and copy-paste the following code.
using Messaging.Common.Events;
using Messaging.Common.Models;
using ProductService.Contracts.Messaging;
using ProductService.Contracts.Models;
using ProductService.Domain.Repositories;
namespace ProductService.Application.Messaging
{
// Implements the core business logic for stock reservation within ProductService.
// This service is called when OrchestratorService publishes a
// StockReservationRequestedEvent (asking ProductService to check and hold stock).
//
// It checks inventory availability, updates stock if possible,
// and returns a StockReservationResult that indicates success or failure.
public class StockReserveService : IStockReserveService
{
// Repository dependency that interacts with the database (Products table)
private readonly IInventoryRepository _repo;
public StockReserveService(IInventoryRepository repo)
{
_repo = repo ?? throw new ArgumentNullException(nameof(repo));
}
// Validates and reserves stock for all products included in the given order.
//
// Steps:
// 1. Validate the request.
// 2. Fetch product data from DB.
// 3. Check available quantity vs requested quantity.
// 4. If enough → decrement stock.
// 5. If not → return a detailed failure result.
public async Task<StockReservationResult> StockReserveAsync(StockReservationRequestedEvent request)
{
// Step 1: Input Validation
// Ensure the request contains at least one item.
if (request.Items == null || request.Items.Count == 0)
{
return new StockReservationResult
{
Success = false,
FailureReason = "No items provided." // No items to process — invalid request.
};
}
// Step 2: Fetch all requested products in one DB query
// Extract distinct product IDs to minimize DB calls.
var productIds = request.Items.Select(i => i.ProductId).Distinct().ToList();
// Get all products from DB for validation.
var products = await _repo.GetProductsByIdsAsync(productIds);
// SIMULATION STEP: Force a Negative Scenario
// Example: Pretend that stock is lower than requested, even if it's not in DB.
// Please uncomment the following to test the cancelled flow
//foreach (var p in products)
//{
// // Artificially set stock to a smaller number to trigger failure.
// p.StockQuantity = 1; // force insufficient stock
//}
// Create a dictionary for lookups by ProductId.
var byId = products.ToDictionary(p => p.Id, p => p);
// Step 3: Validate product existence and stock availability
var failed = new List<FailedLineItem>();
foreach (var line in request.Items)
{
// Case 1: Product does not exist in DB
if (!byId.TryGetValue(line.ProductId, out var product))
{
failed.Add(new FailedLineItem
{
ProductId = line.ProductId,
Requested = line.Quantity,
Available = 0,
Reason = "Product not found"
});
continue;
}
// Case 2: Product exists, but available quantity is less than requested
var available = product.StockQuantity;
if (available < line.Quantity)
{
failed.Add(new FailedLineItem
{
ProductId = line.ProductId,
Requested = line.Quantity,
Available = available,
Reason = "Insufficient stock"
});
}
}
// Step 4: If any validation failures occurred, return failure result
if (failed.Count > 0)
{
return new StockReservationResult
{
Success = false,
FailureReason = "Insufficient stock", // Common reason for all failed items
FailedItems = failed // Detailed per-product failure info
};
}
// Step 5: Perform actual stock deduction for all items
// Prepare a list of tuples like (ProductId, QuantityToReduce)
var decrements = request.Items.Select(i => (i.ProductId, i.Quantity)).ToList();
try
{
// Decrease stock for all items in one transaction.
await _repo.DecreaseStockBulkAsync(decrements);
// Return success response if DB update succeeds.
return new StockReservationResult { Success = true };
}
catch (Exception ex)
{
// Handle any unexpected DB or concurrency errors.
return new StockReservationResult
{
Success = false,
FailureReason = ex.Message // Return the actual error for logging
};
}
}
}
}
ProductService.Infrastructure
This layer handles the Communication and Integration with external systems (RabbitMQ and database repositories). First, create a folder named Messaging. Then, inside the Messaging folder, create three more folders named Producers, Consumers, and Extensions.
Messaging/Consumers/StockReserveConsumer.cs
Consumes StockReservationRequestedEvent published by the Orchestrator.
Flow:
- Receives StockReservationRequestedEvent from the queue.
- Creates a new DI scope (so that scoped services like DbContext work correctly).
- Invokes StockReserveService.StockReserveAsync() to perform the check.
- Based on the result:
-
- If success → Publishes StockReservedCompletedEvent.
- If failure → Publishes StockReservationFailedEvent with details.
-
This is the Entry Point of ProductService into the Saga. It connects RabbitMQ messaging with the internal business logic. So, create a class file named StockReserveConsumer.cs within the Messaging\Consumers folder, and copy-paste the following code.
using Messaging.Common.Consuming;
using Messaging.Common.Events;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ProductService.Contracts.Messaging;
using ProductService.Contracts.Models;
using RabbitMQ.Client;
namespace ProductService.Infrastructure.Messaging.Consumers
{
// RabbitMQ Consumer responsible for handling "StockReservationRequestedEvent" messages.
//
// This class listens to the queue bound to the routing key "stock.reservation.requested"
// (declared in RabbitTopology) and is triggered whenever the OrchestratorService
// requests the ProductService to reserve stock for an order.
//
// Once a message arrives:
// 1. It creates a new DI scope (per message).
// 2. Calls IStockReserveService to perform business logic (check and deduct stock).
// 3. Publishes either StockReservedCompletedEvent or StockReservationFailedEvent
// depending on the result.
public sealed class StockReserveConsumer : BaseConsumer<StockReservationRequestedEvent>
{
// Factory used to create scoped service providers for each message
// (important because DbContext and other services are registered as Scoped)
private readonly IServiceScopeFactory _scopeFactory;
// Constructor — receives shared RabbitMQ channel, queue name, DI scope factory, and logger.
// The base constructor (BaseConsumer) subscribes this consumer to the queue.
public StockReserveConsumer(
IModel channel, // RabbitMQ channel for consuming messages
string queueName, // Queue this consumer listens to
IServiceScopeFactory scopeFactory, // Used to resolve scoped services per message
ILogger<StockReserveConsumer> logger // For logging processing steps/errors
) : base(channel, queueName, logger)
{
_scopeFactory = scopeFactory;
}
// Handles the incoming StockReservationRequestedEvent message.
// Executes inside the background worker defined by BaseConsumer.
// This is where the main stock reservation business logic is triggered.
protected override async Task HandleMessage(StockReservationRequestedEvent message)
{
// STEP 1: Create a per-message dependency injection scope
// Ensures we get fresh scoped instances (e.g., DbContext, repositories, services)
using var scope = _scopeFactory.CreateScope();
// Resolve required application-level services from DI
var inventory = scope.ServiceProvider.GetRequiredService<IStockReserveService>(); // Handles stock check & reservation
var publisher = scope.ServiceProvider.GetRequiredService<IStockReserveEventPublisher>(); // Publishes outcome back to Orchestrator
// STEP 2: Execute the stock reservation logic
// Calls the core application service to check stock and attempt reservation
StockReservationResult result = await inventory.StockReserveAsync(message);
// STEP 3: Based on result, publish appropriate outcome event
if (result.Success)
{
// Success path → build StockReservedCompletedEvent
var success = new StockReservedCompletedEvent
{
CorrelationId = message.CorrelationId,
OrderId = message.OrderId,
UserId = message.UserId,
Items = message.Items // same items confirmed as reserved
};
// Publish success event → consumed by OrchestratorService (StockReservedConsumer)
await publisher.PublishStockReservedCompletedAsync(success);
}
else
{
// Failure path → build StockReservationFailedEvent
var fail = new StockReservationFailedEvent
{
CorrelationId = message.CorrelationId,
OrderId = message.OrderId,
UserId = message.UserId,
Reason = result.FailureReason ?? "Insufficient stock",
FailedItems = result.FailedItems
};
// Publish failure event → consumed by OrchestratorService (StockReservationFailedConsumer)
await publisher.PublishStockReservationFailedAsync(fail);
}
}
}
}
Messaging/Producers/StockReserveEventPublisher.cs
Publishes events back to RabbitMQ once the reservation attempt is completed.
- On success → Publishes StockReservedCompletedEvent.
- On failure → Publishes StockReservationFailedEvent.
Uses IPublisher from the shared Messaging.Common library and RabbitMqOptions for consistent exchange/queue naming. So, create a class file named StockReserveEventPublisher.cs within the Messaging\Producers folder, and copy-paste the following code.
using Messaging.Common.Events;
using Messaging.Common.Options;
using Messaging.Common.Publishing;
using Microsoft.Extensions.Options;
using ProductService.Contracts.Messaging;
namespace ProductService.Infrastructure.Messaging.Producers
{
// Responsible for publishing stock reservation outcome events from ProductService to RabbitMQ.
// When ProductService completes stock reservation (success or failure),
// this class sends the corresponding event message to the correct exchange
// and routing key defined in RabbitMqOptions.
// Published Events:
// 1. StockReservedCompletedEvent → when stock reservation is successful.
// 2. StockReservationFailedEvent → when stock reservation fails.
//
// These messages are consumed by OrchestratorService to continue the Saga flow.
public sealed class StockReserveEventPublisher : IStockReserveEventPublisher
{
// Shared RabbitMQ publisher abstraction (from Messaging.Common)
// Handles serialization, delivery mode, and correlation ID internally.
private readonly IPublisher _publisher;
// RabbitMQ configuration settings (exchange names, routing keys, etc.)
private readonly RabbitMqOptions _opt;
// Constructor that injects the generic publisher and configuration options.
public StockReserveEventPublisher(IPublisher publisher, IOptions<RabbitMqOptions> options)
{
_publisher = publisher;
_opt = options.Value; // Extracts configuration values (e.g., exchange name, routing keys)
}
// Publishes a StockReservedCompletedEvent message when stock reservation succeeds.
// The message is sent to the main topic exchange using the routing key "stock.reserved".
// OrchestratorService’s StockReservedConsumer listens for this event to confirm the order.
public Task PublishStockReservedCompletedAsync(StockReservedCompletedEvent evt)
{
// Sends message to RabbitMQ topic exchange with routing key (stock.reserved)
return _publisher.PublishAsync(
_opt.ExchangeName, // e.g., "ecommerce.topic"
_opt.RkStockReserved, // e.g., "stock.reserved"
evt // Event payload (JSON serialized)
);
}
// Publishes a StockReservationFailedEvent message when stock reservation fails.
// This event notifies the OrchestratorService that ProductService could not
// reserve stock due to insufficient quantity or missing products.
// The Orchestrator then publishes an OrderCancelledEvent to compensate.
public Task PublishStockReservationFailedAsync(StockReservationFailedEvent evt)
{
// Sends message to RabbitMQ topic exchange with routing key (stock.reservation_failed)
return _publisher.PublishAsync(
_opt.ExchangeName, // e.g., "ecommerce.topic"
_opt.RkStockFailed, // e.g., "stock.reservation_failed"
evt // Event payload (contains failure reason and failed items)
);
}
}
}
Messaging/Extensions/RabbitMqConsumerExtensions
Adds an extension method AddStockReserveConsumer() that registers the consumer as a Hosted Service. Simplifies DI configuration in the Program.cs, letting the ProductService automatically subscribe to RabbitMQ queues on startup. So, create a class file named RabbitMqConsumerExtensions.cs within the Messaging\Extensions folder, and copy-paste the following code.
using Microsoft.Extensions.DependencyInjection;
using Messaging.Common.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
using ProductService.Infrastructure.Messaging.Consumers;
namespace ProductService.Infrastructure.Messaging.Extensions
{
// Extension class that cleanly registers the StockReserveConsumer as a hosted background service.
//
// This allows the ProductService to automatically start listening for
// "stock.reservation.requested" messages from RabbitMQ as soon as the app runs.
//
// The extension pattern keeps Program.cs clean by encapsulating consumer registration logic.
public static class RabbitMqConsumerExtensions
{
// Registers the StockReserveConsumer as a hosted service inside the dependency injection (DI) container.
//
// This method is typically called inside Program.cs like:
// builder.Services.AddStockReserveConsumer();
//
// The hosted service will continuously consume messages from the queue defined
// in RabbitMqOptions.QProductStockReservationRequested.
public static IServiceCollection AddStockReserveConsumer(this IServiceCollection services)
{
// Register the consumer as a HostedService → background worker managed by ASP.NET Core.
services.AddHostedService(sp =>
{
// Resolve RabbitMQ Channel (IModel)
// This represents the live connection to RabbitMQ for consuming messages.
var channel = sp.GetRequiredService<IModel>();
// Resolve Scope Factory
// Required to create a new service scope for each message processed.
// (Important because DbContext and repositories are scoped services.)
var scopeFactory = sp.GetRequiredService<IServiceScopeFactory>();
// Resolve Logger
// Injects a typed logger for the StockReserveConsumer to log message processing, errors, etc.
var logger = sp.GetRequiredService<ILogger<StockReserveConsumer>>();
// Resolve RabbitMQ Configuration
// Retrieves queue and exchange names defined in appsettings.json (under "RabbitMq" section).
var options = sp.GetRequiredService<IOptions<RabbitMqOptions>>().Value;
// Create and return StockReserveConsumer instance
// Binds consumer to the correct queue (e.g., "product.stock_reservation_requested")
// and injects all required dependencies.
return new StockReserveConsumer(
channel, // RabbitMQ channel
options.QProductStockReservationRequested, // Queue name this consumer listens to
scopeFactory, // Factory for per-message DI scopes
logger // Logging support
);
});
// Return the IServiceCollection so this can be chained fluently in Program.cs
return services;
}
}
}
ProductService.API
The entry point of the ProductService. It configures all dependencies, including the database, services, and messaging components.
Program.cs
Configures and bootstraps the ProductService for both HTTP and message-based operations. It performs the following steps:
- Registers DbContext for SQL Server.
- Registers all repositories and services.
- Configures RabbitMQ connection using AddRabbitMq().
- Registers:
-
- IPublisher (generic publisher from Messaging.Common).
- IStockReserveEventPublisher (for domain events).
- IStockReserveService (for business logic).
- AddStockReserveConsumer() (to start listening for messages).
-
- Ensures exchange, queues, and bindings exist using RabbitTopology.EnsureAll().
This bootstraps the service by connecting all layers (Application, Infrastructure, and Messaging) so that ProductService can automatically react to Saga events at runtime. So, 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.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using ProductService.Application.Interfaces;
using ProductService.Application.Mappings;
using ProductService.Application.Messaging;
using ProductService.Application.Services;
using ProductService.Contracts.Messaging;
using ProductService.Domain.Repositories;
using ProductService.Infrastructure.Messaging.Extensions;
using ProductService.Infrastructure.Messaging.Producers;
using ProductService.Infrastructure.Persistence;
using ProductService.Infrastructure.Repositories;
using RabbitMQ.Client;
using System.Text.Json.Serialization;
namespace ProductService.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());
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Add DbContext
builder.Services.AddDbContext<ProductDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Register repositories
builder.Services.AddScoped<ICategoryRepository, CategoryRepository>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IProductImageRepository, ProductImageRepository>();
builder.Services.AddScoped<IDiscountRepository, DiscountRepository>();
builder.Services.AddScoped<IInventoryRepository, InventoryRepository>();
builder.Services.AddScoped<IReviewRepository, ReviewRepository>();
// Register services
builder.Services.AddScoped<ICategoryService, CategoryService>();
builder.Services.AddScoped<IProductService, ProductService.Application.Services.ProductService>();
builder.Services.AddScoped<IProductImageService, ProductImageService>();
builder.Services.AddScoped<IDiscountService, DiscountService>();
builder.Services.AddScoped<IReviewService, ReviewService>();
builder.Services.AddScoped<IInventoryService, InventoryService>();
// Add AutoMapper
builder.Services.AddAutoMapper(typeof(MappingProfile));
// -----------------------------------------------------------------------------
// RABBITMQ CONFIGURATION & SAGA PATTERN INTEGRATION SECTION
// -----------------------------------------------------------------------------
// 1️. Load RabbitMQ configuration (from appsettings.json → "RabbitMq" section)
// These options define host, credentials, exchange, routing keys, and queue names
builder.Services.Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq"));
// Extract the strongly-typed RabbitMqOptions object so we can reuse its values directly
var mq = builder.Configuration.GetSection("RabbitMq").Get<RabbitMqOptions>()!;
// 2️. Register RabbitMQ Connection and Channel
// - Uses Messaging.Common.Extensions.AddRabbitMq() to:
// • Create a single long-lived connection to RabbitMQ.
// • Create and register a shared IModel (channel) used by both producers and consumers.
// - This ensures each microservice uses consistent topology and avoids connection leaks.
builder.Services.AddRabbitMq(mq.HostName, mq.UserName, mq.Password, mq.VirtualHost);
// 3️. Register Core Publisher (Infrastructure-Level Abstraction)
// - IPublisher is a shared abstraction defined in Messaging.Common.
// - It handles message serialization, setting correlation IDs, and publishing events
// to the RabbitMQ exchange. All microservices rely on this same class for consistency.
builder.Services.AddSingleton<IPublisher, Publisher>();
// 4️. Register Domain-Specific Publisher (ProductService Responsibility)
// - StockReserveEventPublisher implements IStockReserveEventPublisher
// - It knows *which routing keys* to use for stock-related events, such as:
// • stock.reserved → when stock successfully reserved.
// • stock.reservation_failed → when stock reservation fails.
// - These events are published to the OrchestratorService queues.
builder.Services.AddSingleton<IStockReserveEventPublisher, StockReserveEventPublisher>();
// 5️. Register Application Service for Business Logic
// - StockReserveService contains the actual logic that checks inventory,
// validates quantities, and updates the stock table in the database.
// - The Saga pattern calls this service indirectly through the consumer below.
builder.Services.AddScoped<IStockReserveService, StockReserveService>();
// 6️. Register RabbitMQ Consumer for Incoming Saga Event
// - AddStockReserveConsumer() (defined in ProductService.Infrastructure.Messaging.Extensions)
// registers a hosted background service (StockReserveConsumer) that listens to the queue
// bound to the routing key "stock.reservation.requested".
// - When the OrchestratorService publishes a StockReservationRequestedEvent,
// this consumer receives it, processes the stock reservation,
// and then publishes either StockReservedCompletedEvent or StockReservationFailedEvent.
builder.Services.AddStockReserveConsumer();
var app = builder.Build();
// -----------------------------------------------------------------------------
// TOPOLOGY INITIALIZATION (Ensures Exchange, Queues, and Bindings Exist)
// -----------------------------------------------------------------------------
// 7️. During startup, we explicitly ensure that all exchanges, queues, and bindings
// are declared using RabbitTopology.EnsureAll(). This is idempotent — safe to call multiple times.
// - The ProductService uses the same centralized topology structure as other microservices.
// - Ensures this service can immediately start consuming and publishing messages.
using (var scope = app.Services.CreateScope())
{
var ch = scope.ServiceProvider.GetRequiredService<IModel>(); // Active RabbitMQ channel
var opt = scope.ServiceProvider.GetRequiredService<IOptions<RabbitMqOptions>>().Value; // RabbitMQ options
RabbitTopology.EnsureAll(ch, opt); // Declare exchange, queues, bindings, and DLX/DLQ setup
}
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
appsettings.json
Holds environment-specific configuration values for database connections, logging, and RabbitMQ. Please update the appsettings.json file as follows:
{
"ConnectionStrings": {
"DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ProductServiceDB;Trusted_Connection=True;TrustServerCertificate=True;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"RabbitMq": {
"HostName": "localhost",
"Port": 5672,
"UserName": "ecommerce_user",
"Password": "Test@1234",
"VirtualHost": "ecommerce_vhost"
}
}
In this implementation, the ProductService plays a vital role in ensuring data consistency and transactional integrity within the Saga Pattern. When the Orchestrator requests stock verification, the ProductService performs inventory checks and communicates results via standardized events.

