Refactor email handling for improved structure
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.
This commit is contained in:
48
src/core/DigitalData.MessagingService.Abstraction/Email.cs
Normal file
48
src/core/DigitalData.MessagingService.Abstraction/Email.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
namespace DigitalData.MessagingService.Abstraction;
|
||||||
|
|
||||||
|
public record Email
|
||||||
|
{
|
||||||
|
#if NETFRAMEWORK
|
||||||
|
public EmailAccountDto Sender { get; set; } = null!;
|
||||||
|
#else
|
||||||
|
public required EmailAccountDto Sender { get; init; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recipient email address
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
|
#if NETFRAMEWORK
|
||||||
|
public IEnumerable<string> Recipients { get; set; } = null!;
|
||||||
|
#else
|
||||||
|
public required IEnumerable<string> Recipients { get; init; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Email subject
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
|
#if NETFRAMEWORK
|
||||||
|
public string Subject { get; set; } = null!;
|
||||||
|
#else
|
||||||
|
public required string Subject { get; init; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Email body (HTML or plain text)
|
||||||
|
/// </summary>
|
||||||
|
#if NETFRAMEWORK
|
||||||
|
public string Body { get; set; } = null!;
|
||||||
|
#else
|
||||||
|
public required string Body { get; init; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is HTML email (default: true)
|
||||||
|
/// </summary>
|
||||||
|
#if NETFRAMEWORK
|
||||||
|
public bool IsHtml { get; set; } = true;
|
||||||
|
#else
|
||||||
|
public bool IsHtml { get; init; } = true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -2,29 +2,21 @@
|
|||||||
|
|
||||||
public record SendingEmailEvent
|
public record SendingEmailEvent
|
||||||
{
|
{
|
||||||
|
#if NETFRAMEWORK
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
#else
|
||||||
|
public required Guid Id { get; init; }
|
||||||
|
#endif
|
||||||
|
|
||||||
public EmailAccountDto Sender { get; set; } = null!;
|
#if NETFRAMEWORK
|
||||||
|
public Email Mail { get; set; } = null!;
|
||||||
/// <summary>
|
#else
|
||||||
/// Recipient email address
|
public required Email Mail { get; init; }
|
||||||
/// </summary>
|
#endif
|
||||||
public IEnumerable<string> Recipients { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Email subject
|
|
||||||
/// </summary>
|
|
||||||
public string Subject { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Email body (HTML or plain text)
|
|
||||||
/// </summary>
|
|
||||||
public string Body { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is HTML email (default: true)
|
|
||||||
/// </summary>
|
|
||||||
public bool IsHtml { get; set; } = true;
|
|
||||||
|
|
||||||
|
#if NETFRAMEWORK
|
||||||
public DateTime QueuedAt { get; set; }
|
public DateTime QueuedAt { get; set; }
|
||||||
|
#else
|
||||||
|
public required DateTime QueuedAt { get; init; }
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
@@ -11,11 +11,8 @@ public class EmailMappingProfile : Profile
|
|||||||
{
|
{
|
||||||
public EmailMappingProfile()
|
public EmailMappingProfile()
|
||||||
{
|
{
|
||||||
// SendEmailCommand -> SendingEmailEvent
|
// SendEmailCommand -> Email
|
||||||
// Sender is resolved via MediatR in the handler and set separately after mapping.
|
// Sender is resolved via MediatR in the handler and set separately after mapping.
|
||||||
CreateMap<SendEmailCommand, SendingEmailEvent>()
|
CreateMap<SendEmailCommand, Email>();
|
||||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(_ => Guid.NewGuid()))
|
|
||||||
.ForMember(dest => dest.QueuedAt, opt => opt.MapFrom(_ => DateTime.Now))
|
|
||||||
.ForMember(dest => dest.Sender, opt => opt.Ignore());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace DigitalData.MessagingService.Application.EmailSending.Commands;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command to send an email (enqueue to RabbitMQ)
|
/// Command to send an email (enqueue to RabbitMQ)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record SendEmailCommand : IRequest<SendingEmailEvent>
|
public record SendEmailCommand : IRequest<Guid>
|
||||||
{
|
{
|
||||||
public required GetSenderQuery Sender { get; init; }
|
public required GetSenderQuery Sender { get; init; }
|
||||||
|
|
||||||
@@ -38,21 +38,24 @@ public record SendEmailCommand : IRequest<SendingEmailEvent>
|
|||||||
/// Handler for SendEmailCommand
|
/// Handler for SendEmailCommand
|
||||||
/// Resolves the sender account via MediatR, maps to SendingEmailEvent and enqueues to RabbitMQ
|
/// Resolves the sender account via MediatR, maps to SendingEmailEvent and enqueues to RabbitMQ
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SendEmailCommandHandler(
|
public class SendEmailCommandHandler(ISender Sender, ISendingEmailPublisher Publisher, IMapper Mapper) : IRequestHandler<SendEmailCommand, Guid>
|
||||||
ISender Sender,
|
|
||||||
ISendingEmailPublisher Publisher,
|
|
||||||
IMapper Mapper) : IRequestHandler<SendEmailCommand, SendingEmailEvent>
|
|
||||||
{
|
{
|
||||||
public async Task<SendingEmailEvent> Handle(SendEmailCommand request, CancellationToken cancellationToken)
|
public async Task<Guid> Handle(SendEmailCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var senderAccount = await Sender.Send(request.Sender, cancellationToken)
|
var senderAccount = await Sender.Send(request.Sender, cancellationToken)
|
||||||
?? throw new NotFoundException(
|
?? throw new NotFoundException(
|
||||||
$"No email account found for the given sender criteria (Id: {request.Sender.Id}, Username: {request.Sender.Username}).");
|
$"No email account found for the given sender criteria (Id: {request.Sender.Id}, Username: {request.Sender.Username}).");
|
||||||
|
|
||||||
var sendingEmailEvent = Mapper.Map<SendingEmailEvent>(request) with { Sender = senderAccount };
|
var email = Mapper.Map<Email>(request) with { Sender = senderAccount };
|
||||||
|
|
||||||
// Enqueue to RabbitMQ
|
// Enqueue to RabbitMQ
|
||||||
|
var sendingEmailEvent = new SendingEmailEvent()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Mail = email,
|
||||||
|
QueuedAt = DateTime.Now
|
||||||
|
};
|
||||||
await Publisher.EnqueueAsync(sendingEmailEvent, cancellationToken);
|
await Publisher.EnqueueAsync(sendingEmailEvent, cancellationToken);
|
||||||
return sendingEmailEvent;
|
return sendingEmailEvent.Id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,11 +48,11 @@ public sealed class SendingEmailConsumer : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
// Send email via SMTP (SMTP config is injected in IEmailService via IOptions)
|
// Send email via SMTP (SMTP config is injected in IEmailService via IOptions)
|
||||||
await EmailService.SendEmailAsync(
|
await EmailService.SendEmailAsync(
|
||||||
oMailEvent.Sender,
|
oMailEvent.Mail.Sender,
|
||||||
oMailEvent.Recipients,
|
oMailEvent.Mail.Recipients,
|
||||||
oMailEvent.Subject,
|
oMailEvent.Mail.Subject,
|
||||||
oMailEvent.Body,
|
oMailEvent.Mail.Body,
|
||||||
isHtml: oMailEvent.IsHtml);
|
isHtml: oMailEvent.Mail.IsHtml);
|
||||||
|
|
||||||
// Acknowledge message after successful processing
|
// Acknowledge message after successful processing
|
||||||
await channel.BasicAckAsync(args.DeliveryTag, false, args.CancellationToken);
|
await channel.BasicAckAsync(args.DeliveryTag, false, args.CancellationToken);
|
||||||
@@ -65,7 +65,7 @@ public sealed class SendingEmailConsumer : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger?.LogError(ex, "Failed to process email [To={To}, Subject={Subject}] message: DeliveryTag={DeliveryTag}. Moving to DLQ (NO retry).", oMailEvent?.Recipients, oMailEvent?.Subject, args.DeliveryTag);
|
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
|
// TODO: Error Reporting Strategy
|
||||||
// Option 1: Separate RabbitMQ Queue (emailprofiler.errors)
|
// Option 1: Separate RabbitMQ Queue (emailprofiler.errors)
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ public class EmailsController(IMediator mediator) : ControllerBase
|
|||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
public async Task<IActionResult> SendEmail([FromBody] SendEmailCommand command, CancellationToken cancellationToken)
|
public async Task<IActionResult> SendEmail([FromBody] SendEmailCommand command, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var sendingEmailEvent = await mediator.Send(command, cancellationToken);
|
var eventId = await mediator.Send(command, cancellationToken);
|
||||||
|
|
||||||
return Accepted(new
|
return Accepted(new
|
||||||
{
|
{
|
||||||
sendingEmailEvent.Id,
|
Id = eventId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
using DigitalData.MessagingService.Abstraction;
|
|
||||||
|
|
||||||
namespace DigitalData.MessagingService.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents an outgoing email message to be sent through the messaging service.
|
|
||||||
/// </summary>
|
|
||||||
public record Email
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Recipient email address
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<string> Recipients { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Email subject
|
|
||||||
/// </summary>
|
|
||||||
public string Subject { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Email body (HTML or plain text)
|
|
||||||
/// </summary>
|
|
||||||
public string Body { get; set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is HTML email (default: true)
|
|
||||||
/// </summary>
|
|
||||||
public bool IsHtml { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converts this <see cref="Email"/> instance to an <see cref="SendingEmailEvent"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A new <see cref="SendingEmailEvent"/> representing this email.</returns>
|
|
||||||
internal SendingEmailEvent ToEvent()
|
|
||||||
{
|
|
||||||
return new SendingEmailEvent
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Recipients = Recipients,
|
|
||||||
Subject = Subject,
|
|
||||||
Body = Body,
|
|
||||||
IsHtml = IsHtml,
|
|
||||||
QueuedAt = DateTime.Now
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -125,6 +125,11 @@ public static class EmailSender
|
|||||||
throw new InvalidOperationException("Messaging service is not connected. Call ConnectRabbitMq first.");
|
throw new InvalidOperationException("Messaging service is not connected. Call ConnectRabbitMq first.");
|
||||||
|
|
||||||
var publisher = LazyProvider.Value.GetRequiredService<ISendingEmailPublisher>();
|
var publisher = LazyProvider.Value.GetRequiredService<ISendingEmailPublisher>();
|
||||||
publisher.EnqueueAsync(email.ToEvent());
|
publisher.EnqueueAsync(new SendingEmailEvent()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Mail = email,
|
||||||
|
QueuedAt = DateTime.Now
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +77,7 @@ public sealed class EmailSenderTests
|
|||||||
{
|
{
|
||||||
var email = new Email
|
var email = new Email
|
||||||
{
|
{
|
||||||
|
Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" },
|
||||||
Recipients = ["hakanttek@gmail.com"],
|
Recipients = ["hakanttek@gmail.com"],
|
||||||
Subject = "EmailSender.Send Integration Test",
|
Subject = "EmailSender.Send Integration Test",
|
||||||
Body = "<p>Sent via EmailSender static client.</p>",
|
Body = "<p>Sent via EmailSender static client.</p>",
|
||||||
@@ -93,6 +94,7 @@ public sealed class EmailSenderTests
|
|||||||
{
|
{
|
||||||
var email = new Email
|
var email = new Email
|
||||||
{
|
{
|
||||||
|
Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" },
|
||||||
Recipients = ["hakanttek@gmail.com"],
|
Recipients = ["hakanttek@gmail.com"],
|
||||||
Subject = "Plain Text Test",
|
Subject = "Plain Text Test",
|
||||||
Body = "This is a plain text email.",
|
Body = "This is a plain text email.",
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public sealed class EmailSenderUrlOverloadTests
|
|||||||
{
|
{
|
||||||
var email = new Email
|
var email = new Email
|
||||||
{
|
{
|
||||||
|
Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" },
|
||||||
Recipients = ["url-overload-test@example.com"],
|
Recipients = ["url-overload-test@example.com"],
|
||||||
Subject = "URL Overload Integration Test",
|
Subject = "URL Overload Integration Test",
|
||||||
Body = "<p>Sent after URL-based connection.</p>",
|
Body = "<p>Sent after URL-based connection.</p>",
|
||||||
|
|||||||
@@ -37,10 +37,14 @@ public sealed class SendingEmailPublisherTests : IAsyncDisposable
|
|||||||
var email = new SendingEmailEvent
|
var email = new SendingEmailEvent
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Recipients = ["test@example.com"],
|
Mail = new Email
|
||||||
Subject = "Integration Test - EnqueueAsync",
|
{
|
||||||
Body = "<p>Hello from integration test.</p>",
|
Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" },
|
||||||
IsHtml = true,
|
Recipients = ["test@example.com"],
|
||||||
|
Subject = "Integration Test - EnqueueAsync",
|
||||||
|
Body = "<p>Hello from integration test.</p>",
|
||||||
|
IsHtml = true,
|
||||||
|
},
|
||||||
QueuedAt = DateTime.Now
|
QueuedAt = DateTime.Now
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,10 +66,14 @@ public sealed class SendingEmailPublisherTests : IAsyncDisposable
|
|||||||
var emails = Enumerable.Range(1, 3).Select(i => new SendingEmailEvent
|
var emails = Enumerable.Range(1, 3).Select(i => new SendingEmailEvent
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Recipients = new List<string> { $"recipient{i}@example.com" },
|
Mail = new Email
|
||||||
Subject = $"Integration Test - Batch #{i}",
|
{
|
||||||
Body = $"Batch message {i}",
|
Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" },
|
||||||
IsHtml = false,
|
Recipients = new List<string> { $"recipient{i}@example.com" },
|
||||||
|
Subject = $"Integration Test - Batch #{i}",
|
||||||
|
Body = $"Batch message {i}",
|
||||||
|
IsHtml = false,
|
||||||
|
},
|
||||||
QueuedAt = DateTime.Now
|
QueuedAt = DateTime.Now
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
@@ -85,10 +93,14 @@ public sealed class SendingEmailPublisherTests : IAsyncDisposable
|
|||||||
var email = new SendingEmailEvent
|
var email = new SendingEmailEvent
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Recipients = ["depth-test@example.com"],
|
Mail = new Email
|
||||||
Subject = "Integration Test - GetQueueDepth",
|
{
|
||||||
Body = "Queue depth test",
|
Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" },
|
||||||
IsHtml = false,
|
Recipients = ["depth-test@example.com"],
|
||||||
|
Subject = "Integration Test - GetQueueDepth",
|
||||||
|
Body = "Queue depth test",
|
||||||
|
IsHtml = false,
|
||||||
|
},
|
||||||
QueuedAt = DateTime.Now
|
QueuedAt = DateTime.Now
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,10 +120,14 @@ public sealed class SendingEmailPublisherTests : IAsyncDisposable
|
|||||||
var email = new SendingEmailEvent
|
var email = new SendingEmailEvent
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Recipients = new List<string> { "serialize@example.com" },
|
Mail = new Email
|
||||||
Subject = "Serialization Test",
|
{
|
||||||
Body = "<strong>Bold</strong>",
|
Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" },
|
||||||
IsHtml = true,
|
Recipients = new List<string> { "serialize@example.com" },
|
||||||
|
Subject = "Serialization Test",
|
||||||
|
Body = "<strong>Bold</strong>",
|
||||||
|
IsHtml = true,
|
||||||
|
},
|
||||||
QueuedAt = DateTime.Now
|
QueuedAt = DateTime.Now
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,10 +139,10 @@ public sealed class SendingEmailPublisherTests : IAsyncDisposable
|
|||||||
|
|
||||||
Assert.NotNull(received);
|
Assert.NotNull(received);
|
||||||
Assert.Equal(id, received.Id);
|
Assert.Equal(id, received.Id);
|
||||||
Assert.Equal(["serialize@example.com"], received.Recipients);
|
Assert.Equal(["serialize@example.com"], received.Mail.Recipients);
|
||||||
Assert.Equal("Serialization Test", received.Subject);
|
Assert.Equal("Serialization Test", received.Mail.Subject);
|
||||||
Assert.Equal("<strong>Bold</strong>", received.Body);
|
Assert.Equal("<strong>Bold</strong>", received.Mail.Body);
|
||||||
Assert.True(received.IsHtml);
|
Assert.True(received.Mail.IsHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user