Implementing Saga Pattern in Notification Service

Saga Pattern in Notification Service

In the Saga Orchestration Pattern, the NotificationService acts as the final step in the distributed transaction. Its job is to notify customers about the final status of their order, either Order Confirmed or Order Cancelled, based on events published by the OrchestratorService. This is done Asynchronously using RabbitMQ, maintaining loose coupling and enabling clean scalability for future channels like WhatsApp, Push, etc.

NotificationService.Contracts

The Contracts layer defines the public contracts that other layers or services can rely on.
Here it contains the INotificationServiceHandler interface, which declares methods for handling saga outcome events (OrderConfirmedEvent and OrderCancelledEvent). It provides a uniform contract that both the infrastructure consumers and the application layer can use to consistently invoke notification logic.

Messaging/INotificationServiceHandler.cs

Defines the contract for handling saga outcome notifications. It contains two asynchronous methods:

  • HandleOrderConfirmedAsync(OrderConfirmedEvent evt) → handles successful order notifications.
  • HandleOrderCancelledAsync(OrderCancelledEvent evt) → handles cancellation notifications.

This interface ensures consistent handling logic for both success and failure paths. It acts as the bridge between Infrastructure Consumers and the Application business logic. The consumers only depend on this interface to call appropriate methods. So, create an interface named INotificationServiceHandler.cs within the Messaging folder, and copy-paste the following code.

using Messaging.Common.Events;

namespace NotificationService.Contracts.Messaging
{
    //    This interface defines the high-level contract for the NotificationService
    //    within the Saga Orchestration pattern.
    //    It acts as a bridge between the infrastructure layer (consumers)
    //    and the application layer (where notification logic is executed).
    public interface INotificationServiceHandler
    {
        // HandleOrderConfirmedAsync:
        // This method is called when an OrderConfirmedEvent is received from RabbitMQ.
        // It contains details like OrderId, CustomerName, Email, etc.
        // The implementation will send a "success" notification (email/SMS) to the customer
        // confirming that their order was successfully processed.
        Task HandleOrderConfirmedAsync(OrderConfirmedEvent evt);

        // HandleOrderCancelledAsync:
        // This method is called when an OrderCancelledEvent is received from RabbitMQ.
        // It carries cancellation details such as the reason for failure (e.g., insufficient stock).
        // The implementation will send a "failure/cancellation" notification to the customer
        // informing them that the order could not be completed.
        Task HandleOrderCancelledAsync(OrderCancelledEvent evt);
    }
}

NotificationService.Application

This layer contains the business logic of the notification system. The NotificationServiceHandler class interprets incoming saga events, prepares structured notification data, builds CreateNotificationRequestDTO objects, and passes them to the core INotificationService. It converts raw event payloads (like order ID, customer email, amount, and reason) into application-ready notification requests that can be persisted and sent.

Messaging/NotificationServiceHandler.cs

It implements INotificationServiceHandler. Handles two primary responsibilities:

HandleOrderConfirmedAsync:
  • Receives an OrderConfirmedEvent from RabbitMQ.
  • Builds a structured JSON Items list from the order details.
  • Constructs a CreateNotificationRequestDTO object with placeholders for template rendering (CustomerName, OrderNumber, Amount, Items).
  • Calls INotificationService.CreateAsync() to persist and trigger the notification workflow.
HandleOrderCancelledAsync:
  • Similar to the confirmed case, but includes a “Reason” field in the template data.
  • Sends an order cancellation email/SMS with appropriate content.

This class encapsulates the core business logic for creating notifications in response to Saga outcomes. It converts high-level events into actionable data for the notification domain. So, create a class file named NotificationServiceHandler.cs within the Messaging folder, and copy-paste the following code.

using Messaging.Common.Events;
using NotificationService.Application.DTOs;
using NotificationService.Application.Interfaces;
using NotificationService.Contracts.Messaging;
using NotificationService.Domain.Enums;
using System.Text.Json;

namespace NotificationService.Application.Messaging
{
    // Handles saga outcome events by preparing structured notification requests
    // and delegating them to the NotificationService for delivery (Email/SMS).
    // This class acts as the application-level coordinator between
    // the messaging infrastructure (RabbitMQ consumers) and domain logic.
    public class NotificationServiceHandler : INotificationServiceHandler
    {
        private readonly INotificationService _notificationService;

        // Injects INotificationService via constructor dependency injection.
        // This allows NotificationServiceHandler to call the CreateAsync() method
        // to persist and dispatch customer notifications.
        public NotificationServiceHandler(INotificationService notificationService)
        {
            _notificationService = notificationService;
        }

        // -----------------------------------------------------------------------
        //    HandleOrderConfirmedAsync
        // -----------------------------------------------------------------------
        // This method is called when the Orchestrator publishes an OrderConfirmedEvent.
        // It constructs a structured notification request with template data and recipient info.
        // The result is saved and queued for delivery by the NotificationService.
        public async Task HandleOrderConfirmedAsync(OrderConfirmedEvent evt)
        {
            // STEP 1: Build a structured list of order items.
            // Each item includes ProductId, Quantity, and Price.
            // (If you have product names available, replace ProductId with Name.)
            var items = evt.Items.Select(i => new
            {
                Name = i.ProductId.ToString(),  // Replace with product name if available
                Quantity = i.Quantity,
                Price = i.UnitPrice
            }).ToList();

            // STEP 2: Serialize the items list into JSON format.
            // This allows the templating engine (e.g., Razor/Handlebars) to easily iterate over them.
            var itemsJson = JsonSerializer.Serialize(items);

            // STEP 3: Prepare the template data dictionary.
            // These key-value pairs will be injected into the notification template placeholders.
            var templateData = new Dictionary<string, object>
            {
                { "CustomerName", evt.CustomerName },
                { "OrderNumber", evt.OrderNumber?.ToString() ?? string.Empty },
                { "Amount", evt.TotalAmount },
                { "Items", JsonDocument.Parse(itemsJson).RootElement } // Embed structured items as JSON
            };

            // STEP 4: Build a CreateNotificationRequestDTO object.
            // This DTO contains all metadata needed to send a notification.
            var request = new CreateNotificationRequestDTO
            {
                UserId = evt.UserId,
                TypeId = (int)NotificationTypeEnum.OrderPlaced,   // Notification Type: Order Placed/Confirmed
                Channel = NotificationChannelEnum.Email,          // Medium: Email (can extend to SMS)
                TemplateVersion = 1,                              // Template versioning for flexibility
                TemplateData = templateData,                      // The dynamic content for the message
                Recipients = new List<RecipientDTO>
                {
                    new RecipientDTO
                    {
                        RecipientTypeId = (int)RecipientTypeEnum.To,
                        Email = evt.CustomerEmail,
                        PhoneNumber = evt.PhoneNumber
                    }
                },
                Priority = NotificationPriorityEnum.Normal,        // Priority flag for delivery queue
                ScheduledAtUtc = null,                             // Can be used for delayed notifications
                CreatedBy = "NotificationServiceHandler"           // Audit info for tracking source
            };

            // STEP 5: Persist and dispatch the notification through the core notification service.
            // This will save the notification and trigger downstream delivery (email/SMS).
            await _notificationService.CreateAsync(request);
        }

        // -----------------------------------------------------------------------
        //    HandleOrderCancelledAsync
        // -----------------------------------------------------------------------
        // This method handles the OrderCancelledEvent published by the Orchestrator.
        // It builds a failure notification with cancellation details (e.g., reason).
        public async Task HandleOrderCancelledAsync(OrderCancelledEvent evt)
        {
            // STEP 1: Prepare template placeholders for the cancellation notification.
            var templateData = new Dictionary<string, object>
            {
                { "CustomerName", evt.CustomerName },
                { "OrderNumber", evt.OrderNumber?.ToString() ?? string.Empty },
                { "Amount", evt.TotalAmount },
                { "Reason", evt.Reason }  // Reason for cancellation (e.g., stock shortage)
            };

            // STEP 2: Create the DTO representing a cancellation notification.
            var request = new CreateNotificationRequestDTO
            {
                UserId = evt.UserId,
                TypeId = (int)NotificationTypeEnum.OrderCancelled, // Notification Type: Order Cancelled
                Channel = NotificationChannelEnum.Email,           // Notification medium
                TemplateVersion = 1,                               // Version of the template
                TemplateData = templateData,                       // Dynamic message data
                Recipients = new List<RecipientDTO>
                {
                    new RecipientDTO
                    {
                        RecipientTypeId = (int)RecipientTypeEnum.To,
                        Email = evt.CustomerEmail,
                        PhoneNumber = evt.PhoneNumber
                    }
                },
                Priority = NotificationPriorityEnum.Normal,
                ScheduledAtUtc = null,
                CreatedBy = "NotificationServiceHandler"
            };

            // STEP 3: Save and queue the cancellation notification for delivery.
            await _notificationService.CreateAsync(request);
        }
    }
}

NotificationService.Infrastructure

This layer handles RabbitMQ integration; it consumes saga events, creates a DI scope, and invokes the application handler. OrderConfirmedConsumer and OrderCancelledConsumer are hosted background services that keep listening to the appropriate RabbitMQ queues. It also contains RabbitMqConsumerExtensions, which registers both consumers into the DI container so they start automatically when the service runs.

Messaging/Consumers/OrderConfirmedConsumer.cs

Inherits from BaseConsumer<OrderConfirmedEvent>. Listens to the RabbitMQ queue where OrderConfirmedEvent messages are published by the Orchestrator. On receiving a message:

  • Creates a new dependency injection scope.
  • Resolves INotificationServiceHandler from DI.
  • Calls HandleOrderConfirmedAsync() in the Application layer.

It acts as the infrastructure-level event listener that connects RabbitMQ to business logic. Keeps the consumer lightweight and reusable while delegating actual work to the Application layer. So, create a class file named OrderConfirmedConsumer.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 NotificationService.Contracts.Messaging;
using RabbitMQ.Client;

namespace NotificationService.Infrastructure.Messaging.Consumers
{
    //    This consumer listens for the "OrderConfirmedEvent" messages
    //    that are published by the OrchestratorService once an order
    //    has successfully completed the Saga flow (i.e., stock reserved, order confirmed).
    //    
    //    It belongs to the NotificationService microservice and acts as a bridge
    //    between RabbitMQ (messaging infrastructure) and the application logic
    //    that actually sends notifications to customers.
    public sealed class OrderConfirmedConsumer : BaseConsumer<OrderConfirmedEvent>
    {
        // Used to create a new DI (Dependency Injection) scope for each message.
        // This ensures scoped services (like DbContext or business services)
        // are properly created and disposed per message.
        private readonly IServiceScopeFactory _scopeFactory;

        // Constructor initializes the base consumer with:
        //  - RabbitMQ channel (IModel): to receive and acknowledge messages.
        //  - Queue name: the specific queue this consumer listens to.
        //  - ILogger: for structured logging of message processing.
        public OrderConfirmedConsumer(
            IModel channel,
            string queueName,
            IServiceScopeFactory scopeFactory,
            ILogger<OrderConfirmedConsumer> logger)
            : base(channel, queueName, logger)
        {
            _scopeFactory = scopeFactory;
        }

        // Handles the message logic for "OrderConfirmedEvent".
        // This method is automatically triggered whenever a new message
        // arrives in the configured RabbitMQ queue.
        protected override async Task HandleMessage(OrderConfirmedEvent message)
        {
            Console.WriteLine($"NotificationService [Consumer] → OrderConfirmedConsumer for OrderId={message.OrderId}");

            // STEP 1️: Create a new service scope.
            // This ensures each message gets isolated service instances,
            // avoiding concurrency issues or DbContext lifetime conflicts.
            using var scope = _scopeFactory.CreateScope();

            // STEP 2️: Resolve the application-layer handler from DI.
            // The handler (NotificationServiceHandler) contains the actual
            // business logic for building and sending the notification.
            var app = scope.ServiceProvider.GetRequiredService<INotificationServiceHandler>();

            // STEP 3️: Delegate processing to the application layer.
            // The handler will prepare the notification template data,
            // create a notification entry, and trigger email/SMS delivery.
            await app.HandleOrderConfirmedAsync(message);
        }
    }
}
Messaging/Consumers/OrderCancelledConsumer.cs

Inherits from BaseConsumer<OrderCancelledEvent>. Subscribes to the RabbitMQ queue for cancellation messages. Creates a scoped lifetime for dependencies, resolves the handler, and calls HandleOrderCancelledAsync().

It handles order cancellation notifications (the compensation side of the Saga). Ensures reliability and clean separation between event consumption and logic execution. So, create a class file named OrderCancelledConsumer.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 NotificationService.Contracts.Messaging;
using RabbitMQ.Client;

namespace NotificationService.Infrastructure.Messaging.Consumers
{
    // This class listens for "OrderCancelledEvent" messages
    // that are published by the OrchestratorService.
    //
    // The Orchestrator publishes this event when an order fails
    // during the Saga transaction — for example, when ProductService
    // reports insufficient stock or another step in the workflow fails.
    //
    // Once this consumer receives the message, it triggers the
    // NotificationServiceHandler in the Application layer to send
    // a cancellation notification (via email/SMS) to the customer.
    public sealed class OrderCancelledConsumer : BaseConsumer<OrderCancelledEvent>
    {
        // Used to create a new DI (Dependency Injection) scope per message.
        // Each message will execute in its own scope so that scoped services
        // (like DbContext or other injected dependencies) are safely created
        // and disposed independently.
        private readonly IServiceScopeFactory _scopeFactory;

        // Constructor: Injects all necessary dependencies for consuming messages.
        // - channel: The RabbitMQ channel that connects to the queue.
        // - queueName: The name of the queue this consumer listens to.
        // - scopeFactory: Used to create scoped service providers per message.
        // - logger: Logs message lifecycle events for visibility and debugging.
        public OrderCancelledConsumer(
            IModel channel,
            string queueName,
            IServiceScopeFactory scopeFactory,
            ILogger<OrderCancelledConsumer> logger)
            : base(channel, queueName, logger) // Initialize the base consumer.
        {
            _scopeFactory = scopeFactory;
        }

        // Core logic for processing a single message from RabbitMQ.
        // This method is automatically invoked when a new
        // OrderCancelledEvent arrives in the queue.
        protected override async Task HandleMessage(OrderCancelledEvent message)
        {
            Console.WriteLine($"NotificationService [Consumer] → OrderCancelledConsumer for OrderId={message.OrderId}");

            // STEP 1️: Create a new dependency injection scope.
            // Each message gets its own scope to ensure thread safety
            // and proper disposal of services like DbContext.
            using var scope = _scopeFactory.CreateScope();

            // STEP 2️: Resolve the application-level handler.
            // The handler (NotificationServiceHandler) contains
            // the actual business logic for building and sending
            // a "cancellation" notification to the user.
            var app = scope.ServiceProvider.GetRequiredService<INotificationServiceHandler>();

            // STEP 3️: Delegate message processing to the handler.
            // The handler constructs a structured notification request,
            // fills the appropriate template data (OrderNumber, Reason, etc.),
            // and calls the NotificationService to persist and dispatch it.
            await app.HandleOrderCancelledAsync(message);

            // Once this completes successfully, the BaseConsumer
            // will automatically ACK the message to RabbitMQ,
            // signaling that it was processed without error.
        }
    }
}
Messaging.Extensions/RabbitMqConsumerExtensions

Simplifies consumer registration in Program.cs, the NotificationService just calls services.AddNotificationConsumers() instead of repeating boilerplate code for each consumer.

  • Provides a convenient DI extension method: AddNotificationConsumers().
  • Registers both consumers (OrderConfirmedConsumer and OrderCancelledConsumer) as hosted background services.
  • Each consumer is connected to its respective queue defined in RabbitMqOptions.

So, create a class file named RabbitMqConsumerExtensions.cs within the Messaging\Extensions folder, and copy-paste the following code.

using Messaging.Common.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NotificationService.Infrastructure.Messaging.Consumers;
using RabbitMQ.Client;

namespace NotificationService.Infrastructure.Messaging.Extensions
{
    // This static extension class adds a clean, reusable method to
    // register all RabbitMQ consumers required by the NotificationService.
    //
    // Instead of registering each consumer manually in Program.cs,
    // this keeps the startup configuration organized and centralized.
    //
    // Each consumer is hosted as a background service (IHostedService)
    // and runs continuously, listening for messages from specific queues.
    public static class RabbitMqConsumerExtensions
    {
        // Registers both OrderConfirmedConsumer and OrderCancelledConsumer
        // as hosted background services in the application's dependency injection container.
        //
        // Each hosted service:
        //  - Opens a RabbitMQ channel connection.
        //  - Listens to its respective queue.
        //  - Processes messages via BaseConsumer<T>.
        public static IServiceCollection AddNotificationConsumers(this IServiceCollection services)
        {
            // ------------------------------------------------------------------
            // Register OrderConfirmedConsumer
            // ------------------------------------------------------------------
            services.AddHostedService(sp =>
            {
                // Retrieve RabbitMQ channel (IModel) from DI.
                // This channel is used to receive and acknowledge messages.
                var channel = sp.GetRequiredService<IModel>();

                // Get RabbitMQ configuration (exchange, queue names, etc.)
                var options = sp.GetRequiredService<IOptions<RabbitMqOptions>>().Value;

                // Create a scope factory — allows consumers to create new
                // DI scopes per message, ensuring clean service lifetimes.
                var scopeFactory = sp.GetRequiredService<IServiceScopeFactory>();

                // Get a logger instance for structured logs per consumer.
                var logger = sp.GetRequiredService<ILogger<OrderConfirmedConsumer>>();

                // Create and return a new hosted background consumer
                // for handling "OrderConfirmedEvent" messages.
                return new OrderConfirmedConsumer(
                    channel,
                    options.QNotificationOrderConfirmed, // Queue name from RabbitMqOptions
                    scopeFactory,
                    logger);
            });

            // ------------------------------------------------------------------
            // Register OrderCancelledConsumer
            // ------------------------------------------------------------------
            services.AddHostedService(sp =>
            {
                // Each consumer uses the same pattern:
                // Resolve dependencies from DI for this background process.
                var channel = sp.GetRequiredService<IModel>();
                var options = sp.GetRequiredService<IOptions<RabbitMqOptions>>().Value;
                var scopeFactory = sp.GetRequiredService<IServiceScopeFactory>();
                var logger = sp.GetRequiredService<ILogger<OrderCancelledConsumer>>();

                // Create a hosted background consumer for "OrderCancelledEvent".
                return new OrderCancelledConsumer(
                    channel,
                    options.QNotificationOrderCancelled, // Queue for order cancellation notifications
                    scopeFactory,
                    logger);
            });

            // ------------------------------------------------------------------
            // Return IServiceCollection so that this method can be chained fluently
            // in Program.cs during application startup.
            // Example usage:
            //     builder.Services.AddNotificationConsumers();
            // ------------------------------------------------------------------
            return services;
        }
    }
}

NotificationService.API

The API project hosts the web application. Its Program.cs configures dependency injection, registers RabbitMQ connections, application services, repositories, background workers, and the notification consumers. It ensures the topology (exchange, queues, and bindings) exists before starting the app. Although it exposes controllers for future health checks, this service operates mainly as a background message processor.

Program.cs

This is the central configuration point that binds all layers together. It ensures that as soon as the microservice starts, it’s fully connected to the RabbitMQ event bus and ready to respond to Orchestrator events.

Acts as the bootstrapper for the NotificationService. Configures:

  • DbContext for persistence.
  • Repositories for templates, preferences, and notifications.
  • Notification workers for background email/SMS sending.
  • RabbitMQ setup via AddRabbitMq() from Messaging.Common.
  • Consumers via AddNotificationConsumers().

Calls RabbitTopology.EnsureAll() at startup to ensure all RabbitMQ entities (exchanges, queues, DLQs) exist. So, please modify the Program class file as follows.

using Messaging.Common.Extensions;
using Messaging.Common.Options;
using Messaging.Common.Topology;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using NotificationService.Application.Handlers;
using NotificationService.Application.Interfaces;
using NotificationService.Application.Messaging;
using NotificationService.Application.Services;
using NotificationService.Application.Utilities;
using NotificationService.Contracts.Interfaces;
using NotificationService.Contracts.Messaging;
using NotificationService.Domain.Repositories;
using NotificationService.Infrastructure.BackgroundJobs;
using NotificationService.Infrastructure.Messaging.Extensions;
using NotificationService.Infrastructure.Persistence;
using NotificationService.Infrastructure.Repositories;
using RabbitMQ.Client;
using System.Text.Json.Serialization;

namespace NotificationService.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();

            // Add DbContext
            builder.Services.AddDbContext<NotificationDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

            builder.Services.AddScoped<INotificationService, NotificationService.Application.Services.NotificationService>();
            builder.Services.AddScoped<IPreferenceService, PreferenceService>();
            builder.Services.AddScoped<IEmailService, EmailService>();
            builder.Services.AddScoped<ISMSService, SMSService>();
            builder.Services.AddScoped<ITemplateService, TemplateService>();
            builder.Services.AddScoped<ITemplateRenderer, TemplateRenderer>();

            // Channel Handlers
            builder.Services.AddScoped<INotificationChannelHandler, EmailChannelHandler>();
            builder.Services.AddScoped<INotificationChannelHandler, SmsChannelHandler>();
            builder.Services.AddScoped<INotificationChannelHandler, InAppChannelHandler>();

            //Repositories
            builder.Services.AddScoped<INotificationRepository, NotificationRepository>();
            builder.Services.AddScoped<INotificationTemplateRepository, NotificationTemplateRepository>();
            builder.Services.AddScoped<IUserPreferenceRepository, UserPreferenceRepository>();

            // Register Application service
            builder.Services.AddScoped<INotificationProcessor, NotificationService.Application.Services.NotificationService>();

            // Register Background Worker
            builder.Services.AddHostedService<NotificationWorker>();

            // ============================================================
            // RABBITMQ CONFIGURATION SECTION (SAGA PATTERN INTEGRATION)
            // ============================================================

            // 1️ Load RabbitMQ configuration from appsettings.json into strongly-typed RabbitMqOptions.
            //    This includes settings like host, username, exchange, and queue names.
            builder.Services.Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMq"));

            // 2️ Retrieve the configuration immediately for topology setup (exchange/queue declarations).
            var mq = builder.Configuration.GetSection("RabbitMq").Get<RabbitMqOptions>()!;

            // 3️ Register RabbitMQ Connection + Channel into the DI container.
            //    This uses a shared extension from Messaging.Common.
            //    - Creates a long-lived connection to RabbitMQ (ConnectionManager)
            //    - Opens a channel (IModel) for publishing and consuming
            //    - Registers both as singletons, reused throughout the service lifetime
            builder.Services.AddRabbitMq(mq.HostName, mq.UserName, mq.Password, mq.VirtualHost);

            // 4️ Register the application-level message handler.
            //    This class (NotificationServiceHandler) receives events from consumers
            //    and handles the business logic for creating and sending notifications.
            builder.Services.AddScoped<INotificationServiceHandler, NotificationServiceHandler>();

            // 5️ Register the RabbitMQ consumers for Saga events.
            //    - OrderConfirmedConsumer → listens to "order.confirmed" messages.
            //    - OrderCancelledConsumer → listens to "order.cancelled" messages.
            //    Each consumer runs as a hosted background service that reacts
            //    automatically to messages published by the OrchestratorService.
            builder.Services.AddNotificationConsumers();

            var app = builder.Build();

            // ============================================================
            // ENSURE RABBITMQ TOPOLOGY (EXCHANGES, QUEUES, BINDINGS)
            // ============================================================
            // On application startup, ensure that the RabbitMQ topology exists.
            // This step:
            //    - Declares the main topic exchange (ecommerce.topic)
            //    - Declares the dead-letter exchange (DLX)
            //    - Creates queues for all consumers (order.confirmed, order.cancelled)
            //    - Binds them to their corresponding routing keys.
            // This is idempotent → safe to call multiple times without duplicating resources.
            using (var scope = app.Services.CreateScope())
            {
                var ch = scope.ServiceProvider.GetRequiredService<IModel>();
                var opt = scope.ServiceProvider.GetRequiredService<IOptions<RabbitMqOptions>>().Value;
                RabbitTopology.EnsureAll(ch, opt);
            }

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            app.UseAuthorization();

            app.MapControllers();

            app.Run();
        }
    }
}
appsettings.json for NotificationService

Holds environment-specific external service configuration in a central place. It configures:

  • RabbitMQ: Host, Username, Password, VHost.
  • SQL Connection: NotificationServiceDB.
  • SMTP (Gmail): Sender name/email, app password.
  • Twilio: SMS configuration.

So, please modify the appsetings.json file as follows.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=NotificationServiceDB;Trusted_Connection=True;TrustServerCertificate=True;"
  },
  "EmailSettings": {
    "SmtpServer": "smtp.gmail.com",
    "SmtpPort": "587",
    "SenderName": "Dot Net Tutorials",
    "SenderEmail": "[SenderEmail]",
    "AppPassword": "[AppPassword]"
  },
  "SMSSettings": {
    "AccountSID": "[AccountSID]",
    "AuthToken": "[AuthToken]",
    "FromNumber": "[FromNumber]"
  },
  "RabbitMq": {
    "HostName": "localhost",
    "Port": 5672,
    "UserName": "ecommerce_user",
    "Password": "Test@1234",
    "VirtualHost": "ecommerce_vhost"
  }
}

The NotificationService completes the Saga Orchestration cycle by informing the customer about their order’s final status, success or cancellation, based on events from the OrchestratorService. By separating responsibilities across layers:

  • Contracts define the interfaces,
  • Application executes the business logic,
  • Infrastructure handles event consumption,
  • API configures and boots the system,

you maintain a highly modular, testable, and extensible architecture. This design ensures that future notification channels (e.g., WhatsApp, Push, In-App) can be integrated easily without changing the existing order or product workflows.

Leave a Reply

Your email address will not be published. Required fields are marked *