Refactored `Email` and `SendingEmailEvent` to use `record` types, consolidating email-related data into the `Email` class. Updated `SendEmailCommand` to return a `Guid` and simplified mapping logic in `EmailMappingProfile`. Adjusted `SendEmailCommandHandler` to construct `SendingEmailEvent` manually. Updated `SendingEmailConsumer`, `EmailsController`, and `EmailSender` to reflect the new structure. Removed the old `Email` implementation. Improved logging to reference the `Mail` property. Revised tests to align with the new structure, ensuring immutability and better separation of concerns.
132 lines
5.7 KiB
C#
132 lines
5.7 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using DigitalData.MessagingService.Application.Common.Interfaces;
|
|
using DigitalData.MessagingService.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 SendingEmailConsumer : IAsyncDisposable
|
|
{
|
|
private readonly RabbitMqConfiguration _config;
|
|
|
|
private readonly Lazy<Task<IChannel>> _lazyChannel;
|
|
|
|
private readonly Lazy<Task> _lazyInit;
|
|
|
|
private readonly ILogger<SendingEmailConsumer>? _logger;
|
|
|
|
public SendingEmailConsumer(IOptions<RabbitMqConfiguration> config, IEmailService EmailService, RabbitMqConnectionFactory CnnFactory, ILogger<SendingEmailConsumer>? 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) =>
|
|
{
|
|
SendingEmailEvent? oMailEvent = null;
|
|
try
|
|
{
|
|
var json = Encoding.UTF8.GetString(args.Body.ToArray());
|
|
oMailEvent = JsonSerializer.Deserialize<SendingEmailEvent>(json);
|
|
|
|
if (oMailEvent is not null)
|
|
{
|
|
// Send email via SMTP (SMTP config is injected in IEmailService via IOptions)
|
|
await EmailService.SendEmailAsync(
|
|
oMailEvent.Mail.Sender,
|
|
oMailEvent.Mail.Recipients,
|
|
oMailEvent.Mail.Subject,
|
|
oMailEvent.Mail.Body,
|
|
isHtml: oMailEvent.Mail.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?.Mail.Recipients, oMailEvent?.Mail.Subject, args.DeliveryTag);
|
|
|
|
// TODO: Error Reporting Strategy
|
|
// Option 1: Separate RabbitMQ Queue (emailprofiler.errors)
|
|
// - Create EmailErrorReport entity { SendingEmailEventId, 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("SendingEmailConsumer 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();
|
|
}
|
|
}
|
|
}
|