Reorganized the solution structure to align with a layered architecture: - Replaced `src` folder with `core`, `infrastructure`, and `presentation`. - Moved projects to their respective folders. - Added `DigitalData.MessagingService.Publisher.Abstraction` project. - Removed `DigitalData.MessagingService.Client` project. Updated project configurations and nesting in the solution file. Added `appsettings.Secrets.json` with RabbitMQ and email account settings: - RabbitMQ configuration includes hostname, port, credentials, and queue/exchange details. - Email configuration includes SMTP server details and credentials.
131 lines
5.6 KiB
C#
131 lines
5.6 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using DigitalData.MessagingService.Application.Common.Interfaces;
|
|
using DigitalData.MessagingService.Publisher.Abstraction;
|
|
using DigitalData.MessagingService.RabbitMQ;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using RabbitMQ.Client;
|
|
using RabbitMQ.Client.Events;
|
|
|
|
namespace DigitalData.MessagingService.Infrastructure.Queue;
|
|
|
|
/// <summary>
|
|
/// RabbitMQ-based email queue implementation for outgoing emails.
|
|
/// Provides message persistence, scalability, and reliability.
|
|
/// Uses Lazy<T> initialization pattern to avoid blocking constructor.
|
|
/// </summary>
|
|
public sealed class OutgoingEmailConsumer : IAsyncDisposable
|
|
{
|
|
private readonly RabbitMqConfiguration _config;
|
|
|
|
private readonly Lazy<Task<IChannel>> _lazyChannel;
|
|
|
|
private readonly Lazy<Task> _lazyInit;
|
|
|
|
private readonly ILogger<OutgoingEmailConsumer>? _logger;
|
|
|
|
public OutgoingEmailConsumer(IOptions<RabbitMqConfiguration> config, IEmailService EmailService, RabbitMqConnectionFactory CnnFactory, ILogger<OutgoingEmailConsumer>? logger = null)
|
|
{
|
|
_logger = logger;
|
|
_config = config.Value;
|
|
|
|
_lazyChannel = new(CnnFactory.CreateChannelAsync);
|
|
_lazyInit = new(async () => {
|
|
var channel = await _lazyChannel.Value;
|
|
|
|
var consumer = new AsyncEventingBasicConsumer(channel);
|
|
|
|
consumer.ReceivedAsync += async (sender, args) =>
|
|
{
|
|
OutgoingEmailEvent? oMailEvent = null;
|
|
try
|
|
{
|
|
var json = Encoding.UTF8.GetString(args.Body.ToArray());
|
|
oMailEvent = JsonSerializer.Deserialize<OutgoingEmailEvent>(json);
|
|
|
|
if (oMailEvent is not null)
|
|
{
|
|
// Send email via SMTP (SMTP config is injected in IEmailService via IOptions)
|
|
await EmailService.SendEmailAsync(
|
|
oMailEvent.Recipient,
|
|
oMailEvent.Subject,
|
|
oMailEvent.Body,
|
|
isHtml: oMailEvent.IsHtml);
|
|
|
|
// Acknowledge message after successful processing
|
|
await channel.BasicAckAsync(args.DeliveryTag, false, args.CancellationToken);
|
|
}
|
|
else
|
|
{
|
|
logger?.LogWarning("Failed to deserialize email message: DeliveryTag={DeliveryTag}", args.DeliveryTag);
|
|
await channel.BasicNackAsync(args.DeliveryTag, false, false, args.CancellationToken); // Don't requeue invalid messages
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger?.LogError(ex, "Failed to process email [To={To}, Subject={Subject}] message: DeliveryTag={DeliveryTag}. Moving to DLQ (NO retry).", oMailEvent?.Recipient, oMailEvent?.Subject, args.DeliveryTag);
|
|
|
|
// TODO: Error Reporting Strategy
|
|
// Option 1: Separate RabbitMQ Queue (emailprofiler.errors)
|
|
// - Create EmailErrorReport entity { OutgoingEmailEventId, Exception, StackTrace, Timestamp, RetryAttempt }
|
|
// - Publish to error queue: await _errorQueue.EnqueueAsync(errorReport)
|
|
// - Separate worker processes error queue → Log to DB/File/External monitoring
|
|
//
|
|
// Option 2: Database Table (TBEMLP_ERROR_LOG)
|
|
// - Columns: ERROR_ID, OUTBOX_ID, ERROR_MESSAGE, STACK_TRACE, ERROR_DATE
|
|
// - Insert via IErrorLogRepository.CreateAsync(errorLog)
|
|
//
|
|
// Option 3: External Monitoring Service
|
|
// - Sentry: SentrySdk.CaptureException(ex)
|
|
// - Application Insights: _telemetryClient.TrackException(ex)
|
|
// - Elasticsearch: _elasticClient.IndexDocument(errorLog)
|
|
//
|
|
// Recommended: Option 1 (RabbitMQ Error Queue) + Option 2 (DB persistence)
|
|
// - Fast async error logging (non-blocking)
|
|
// - Persistent storage for audit
|
|
// - Real-time alerting via monitoring worker
|
|
|
|
// NO RETRY - All failures move directly to DLQ
|
|
await channel.BasicNackAsync(args.DeliveryTag, false, false, args.CancellationToken); // requeue=false → DLQ
|
|
}
|
|
};
|
|
|
|
// Start consuming messages (event-driven, non-blocking)
|
|
await channel.BasicConsumeAsync(
|
|
queue: _config.QueueName,
|
|
autoAck: false,
|
|
consumer: consumer,
|
|
cancellationToken: CnnFactory.CancellationToken);
|
|
|
|
logger?.LogInformation("RabbitMQ consumer started for queue: {QueueName}", _config.QueueName);
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize RabbitMQ connection, channel, exchanges, and queues asynchronously.
|
|
/// Start event-driven consumer that processes messages as they arrive
|
|
/// Called lazily on first use via EnsureInitializedAsync.
|
|
/// </summary>
|
|
public async Task InitAsync()
|
|
{
|
|
if (_lazyInit.IsValueCreated)
|
|
_logger?.LogWarning("OutgoingEmailConsumer already initialized. InitAsync() called multiple times.");
|
|
|
|
await _lazyInit.Value;
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (!_lazyChannel.IsValueCreated)
|
|
return;
|
|
|
|
var channel = await _lazyChannel.Value;
|
|
if (channel is not null)
|
|
{
|
|
await channel.CloseAsync();
|
|
await channel.DisposeAsync();
|
|
}
|
|
}
|
|
}
|