From c6e67c0f99d4456b3679b8cbcd2325ef8b1fcc4b Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 5 Aug 2026 13:59:54 +0200 Subject: [PATCH] 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. --- .../Email.cs | 48 ++++++++++++++++ .../SendingEmailEvent.cs | 34 +++++------ .../Common/Mappings/EmailMappingProfile.cs | 7 +-- .../EmailSending/Commands/SendEmailCommand.cs | 21 ++++--- .../Queue/SendingEmailConsumer.cs | 12 ++-- .../Controllers/EmailsController.cs | 4 +- .../Email.cs | 46 --------------- .../EmailSender.cs | 7 ++- .../Integration/EmailSenderTests.cs | 2 + .../EmailSenderUrlOverloadTests.cs | 1 + .../Integration/SendingEmailPublisherTests.cs | 56 ++++++++++++------- 11 files changed, 128 insertions(+), 110 deletions(-) create mode 100644 src/core/DigitalData.MessagingService.Abstraction/Email.cs delete mode 100644 src/presentation/DigitalData.MessagingService.Client/Email.cs diff --git a/src/core/DigitalData.MessagingService.Abstraction/Email.cs b/src/core/DigitalData.MessagingService.Abstraction/Email.cs new file mode 100644 index 0000000..ab1650c --- /dev/null +++ b/src/core/DigitalData.MessagingService.Abstraction/Email.cs @@ -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 + + /// + /// Recipient email address + /// + +#if NETFRAMEWORK + public IEnumerable Recipients { get; set; } = null!; +#else + public required IEnumerable Recipients { get; init; } +#endif + + /// + /// Email subject + /// + +#if NETFRAMEWORK + public string Subject { get; set; } = null!; +#else + public required string Subject { get; init; } +#endif + + /// + /// Email body (HTML or plain text) + /// +#if NETFRAMEWORK + public string Body { get; set; } = null!; +#else + public required string Body { get; init; } +#endif + + /// + /// Is HTML email (default: true) + /// +#if NETFRAMEWORK + public bool IsHtml { get; set; } = true; +#else + public bool IsHtml { get; init; } = true; +#endif +} \ No newline at end of file diff --git a/src/core/DigitalData.MessagingService.Abstraction/SendingEmailEvent.cs b/src/core/DigitalData.MessagingService.Abstraction/SendingEmailEvent.cs index 76af7eb..4093cf1 100644 --- a/src/core/DigitalData.MessagingService.Abstraction/SendingEmailEvent.cs +++ b/src/core/DigitalData.MessagingService.Abstraction/SendingEmailEvent.cs @@ -2,29 +2,21 @@ public record SendingEmailEvent { +#if NETFRAMEWORK public Guid Id { get; set; } +#else + public required Guid Id { get; init; } +#endif - public EmailAccountDto Sender { get; set; } = null!; - - /// - /// Recipient email address - /// - public IEnumerable Recipients { get; set; } = null!; - - /// - /// Email subject - /// - public string Subject { get; set; } = null!; - - /// - /// Email body (HTML or plain text) - /// - public string Body { get; set; } = null!; - - /// - /// Is HTML email (default: true) - /// - public bool IsHtml { get; set; } = true; +#if NETFRAMEWORK + public Email Mail { get; set; } = null!; +#else + public required Email Mail { get; init; } +#endif +#if NETFRAMEWORK public DateTime QueuedAt { get; set; } +#else + public required DateTime QueuedAt { get; init; } +#endif } \ No newline at end of file diff --git a/src/core/DigitalData.MessagingService.Application/Common/Mappings/EmailMappingProfile.cs b/src/core/DigitalData.MessagingService.Application/Common/Mappings/EmailMappingProfile.cs index b3bf7de..3972ab6 100644 --- a/src/core/DigitalData.MessagingService.Application/Common/Mappings/EmailMappingProfile.cs +++ b/src/core/DigitalData.MessagingService.Application/Common/Mappings/EmailMappingProfile.cs @@ -11,11 +11,8 @@ public class EmailMappingProfile : Profile { public EmailMappingProfile() { - // SendEmailCommand -> SendingEmailEvent + // SendEmailCommand -> Email // Sender is resolved via MediatR in the handler and set separately after mapping. - CreateMap() - .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()); + CreateMap(); } } diff --git a/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/SendEmailCommand.cs b/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/SendEmailCommand.cs index 240a891..f4a7e77 100644 --- a/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/SendEmailCommand.cs +++ b/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/SendEmailCommand.cs @@ -9,7 +9,7 @@ namespace DigitalData.MessagingService.Application.EmailSending.Commands; /// /// Command to send an email (enqueue to RabbitMQ) /// -public record SendEmailCommand : IRequest +public record SendEmailCommand : IRequest { public required GetSenderQuery Sender { get; init; } @@ -38,21 +38,24 @@ public record SendEmailCommand : IRequest /// Handler for SendEmailCommand /// Resolves the sender account via MediatR, maps to SendingEmailEvent and enqueues to RabbitMQ /// -public class SendEmailCommandHandler( -ISender Sender, -ISendingEmailPublisher Publisher, -IMapper Mapper) : IRequestHandler +public class SendEmailCommandHandler(ISender Sender, ISendingEmailPublisher Publisher, IMapper Mapper) : IRequestHandler { - public async Task Handle(SendEmailCommand request, CancellationToken cancellationToken) + public async Task Handle(SendEmailCommand request, CancellationToken cancellationToken) { var senderAccount = await Sender.Send(request.Sender, cancellationToken) ?? throw new NotFoundException( $"No email account found for the given sender criteria (Id: {request.Sender.Id}, Username: {request.Sender.Username})."); - var sendingEmailEvent = Mapper.Map(request) with { Sender = senderAccount }; + var email = Mapper.Map(request) with { Sender = senderAccount }; // Enqueue to RabbitMQ + var sendingEmailEvent = new SendingEmailEvent() + { + Id = Guid.NewGuid(), + Mail = email, + QueuedAt = DateTime.Now + }; await Publisher.EnqueueAsync(sendingEmailEvent, cancellationToken); - return sendingEmailEvent; + return sendingEmailEvent.Id; } -} +} \ No newline at end of file diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Queue/SendingEmailConsumer.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Queue/SendingEmailConsumer.cs index 0bf9cd1..596835d 100644 --- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Queue/SendingEmailConsumer.cs +++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Queue/SendingEmailConsumer.cs @@ -48,11 +48,11 @@ public sealed class SendingEmailConsumer : IAsyncDisposable { // Send email via SMTP (SMTP config is injected in IEmailService via IOptions) await EmailService.SendEmailAsync( - oMailEvent.Sender, - oMailEvent.Recipients, - oMailEvent.Subject, - oMailEvent.Body, - isHtml: oMailEvent.IsHtml); + 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); @@ -65,7 +65,7 @@ public sealed class SendingEmailConsumer : IAsyncDisposable } 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 // Option 1: Separate RabbitMQ Queue (emailprofiler.errors) diff --git a/src/presentation/DigitalData.MessagingService.API/Controllers/EmailsController.cs b/src/presentation/DigitalData.MessagingService.API/Controllers/EmailsController.cs index fde87cd..080b433 100644 --- a/src/presentation/DigitalData.MessagingService.API/Controllers/EmailsController.cs +++ b/src/presentation/DigitalData.MessagingService.API/Controllers/EmailsController.cs @@ -23,11 +23,11 @@ public class EmailsController(IMediator mediator) : ControllerBase [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task SendEmail([FromBody] SendEmailCommand command, CancellationToken cancellationToken) { - var sendingEmailEvent = await mediator.Send(command, cancellationToken); + var eventId = await mediator.Send(command, cancellationToken); return Accepted(new { - sendingEmailEvent.Id, + Id = eventId }); } } diff --git a/src/presentation/DigitalData.MessagingService.Client/Email.cs b/src/presentation/DigitalData.MessagingService.Client/Email.cs deleted file mode 100644 index bfd6f2b..0000000 --- a/src/presentation/DigitalData.MessagingService.Client/Email.cs +++ /dev/null @@ -1,46 +0,0 @@ -using DigitalData.MessagingService.Abstraction; - -namespace DigitalData.MessagingService.Client; - -/// -/// Represents an outgoing email message to be sent through the messaging service. -/// -public record Email -{ - /// - /// Recipient email address - /// - public IEnumerable Recipients { get; set; } = null!; - - /// - /// Email subject - /// - public string Subject { get; set; } = null!; - - /// - /// Email body (HTML or plain text) - /// - public string Body { get; set; } = null!; - - /// - /// Is HTML email (default: true) - /// - public bool IsHtml { get; set; } = true; - - /// - /// Converts this instance to an . - /// - /// A new representing this email. - internal SendingEmailEvent ToEvent() - { - return new SendingEmailEvent - { - Id = Guid.NewGuid(), - Recipients = Recipients, - Subject = Subject, - Body = Body, - IsHtml = IsHtml, - QueuedAt = DateTime.Now - }; - } -} \ No newline at end of file diff --git a/src/presentation/DigitalData.MessagingService.Client/EmailSender.cs b/src/presentation/DigitalData.MessagingService.Client/EmailSender.cs index 81a3502..7a817e4 100644 --- a/src/presentation/DigitalData.MessagingService.Client/EmailSender.cs +++ b/src/presentation/DigitalData.MessagingService.Client/EmailSender.cs @@ -125,6 +125,11 @@ public static class EmailSender throw new InvalidOperationException("Messaging service is not connected. Call ConnectRabbitMq first."); var publisher = LazyProvider.Value.GetRequiredService(); - publisher.EnqueueAsync(email.ToEvent()); + publisher.EnqueueAsync(new SendingEmailEvent() + { + Id = Guid.NewGuid(), + Mail = email, + QueuedAt = DateTime.Now + }); } } \ No newline at end of file diff --git a/tests/DigitalData.MessagingService.Tests/Integration/EmailSenderTests.cs b/tests/DigitalData.MessagingService.Tests/Integration/EmailSenderTests.cs index b69df3f..13ba0a9 100644 --- a/tests/DigitalData.MessagingService.Tests/Integration/EmailSenderTests.cs +++ b/tests/DigitalData.MessagingService.Tests/Integration/EmailSenderTests.cs @@ -77,6 +77,7 @@ public sealed class EmailSenderTests { var email = new Email { + Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" }, Recipients = ["hakanttek@gmail.com"], Subject = "EmailSender.Send Integration Test", Body = "

Sent via EmailSender static client.

", @@ -93,6 +94,7 @@ public sealed class EmailSenderTests { var email = new Email { + Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" }, Recipients = ["hakanttek@gmail.com"], Subject = "Plain Text Test", Body = "This is a plain text email.", diff --git a/tests/DigitalData.MessagingService.Tests/Integration/EmailSenderUrlOverloadTests.cs b/tests/DigitalData.MessagingService.Tests/Integration/EmailSenderUrlOverloadTests.cs index a145eff..46abc8e 100644 --- a/tests/DigitalData.MessagingService.Tests/Integration/EmailSenderUrlOverloadTests.cs +++ b/tests/DigitalData.MessagingService.Tests/Integration/EmailSenderUrlOverloadTests.cs @@ -57,6 +57,7 @@ public sealed class EmailSenderUrlOverloadTests { var email = new Email { + Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" }, Recipients = ["url-overload-test@example.com"], Subject = "URL Overload Integration Test", Body = "

Sent after URL-based connection.

", diff --git a/tests/DigitalData.MessagingService.Tests/Integration/SendingEmailPublisherTests.cs b/tests/DigitalData.MessagingService.Tests/Integration/SendingEmailPublisherTests.cs index b4fd0d7..506c4ff 100644 --- a/tests/DigitalData.MessagingService.Tests/Integration/SendingEmailPublisherTests.cs +++ b/tests/DigitalData.MessagingService.Tests/Integration/SendingEmailPublisherTests.cs @@ -37,10 +37,14 @@ public sealed class SendingEmailPublisherTests : IAsyncDisposable var email = new SendingEmailEvent { Id = Guid.NewGuid(), - Recipients = ["test@example.com"], - Subject = "Integration Test - EnqueueAsync", - Body = "

Hello from integration test.

", - IsHtml = true, + Mail = new Email + { + Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" }, + Recipients = ["test@example.com"], + Subject = "Integration Test - EnqueueAsync", + Body = "

Hello from integration test.

", + IsHtml = true, + }, QueuedAt = DateTime.Now }; @@ -62,10 +66,14 @@ public sealed class SendingEmailPublisherTests : IAsyncDisposable var emails = Enumerable.Range(1, 3).Select(i => new SendingEmailEvent { Id = Guid.NewGuid(), - Recipients = new List { $"recipient{i}@example.com" }, - Subject = $"Integration Test - Batch #{i}", - Body = $"Batch message {i}", - IsHtml = false, + Mail = new Email + { + Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" }, + Recipients = new List { $"recipient{i}@example.com" }, + Subject = $"Integration Test - Batch #{i}", + Body = $"Batch message {i}", + IsHtml = false, + }, QueuedAt = DateTime.Now }).ToList(); @@ -85,10 +93,14 @@ public sealed class SendingEmailPublisherTests : IAsyncDisposable var email = new SendingEmailEvent { Id = Guid.NewGuid(), - Recipients = ["depth-test@example.com"], - Subject = "Integration Test - GetQueueDepth", - Body = "Queue depth test", - IsHtml = false, + Mail = new Email + { + Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" }, + Recipients = ["depth-test@example.com"], + Subject = "Integration Test - GetQueueDepth", + Body = "Queue depth test", + IsHtml = false, + }, QueuedAt = DateTime.Now }; @@ -108,10 +120,14 @@ public sealed class SendingEmailPublisherTests : IAsyncDisposable var email = new SendingEmailEvent { Id = id, - Recipients = new List { "serialize@example.com" }, - Subject = "Serialization Test", - Body = "Bold", - IsHtml = true, + Mail = new Email + { + Sender = new EmailAccountDto { Username = "test@example.com", Password = "password", SmtpServer = "smtp.example.com" }, + Recipients = new List { "serialize@example.com" }, + Subject = "Serialization Test", + Body = "Bold", + IsHtml = true, + }, QueuedAt = DateTime.Now }; @@ -123,10 +139,10 @@ public sealed class SendingEmailPublisherTests : IAsyncDisposable Assert.NotNull(received); Assert.Equal(id, received.Id); - Assert.Equal(["serialize@example.com"], received.Recipients); - Assert.Equal("Serialization Test", received.Subject); - Assert.Equal("Bold", received.Body); - Assert.True(received.IsHtml); + Assert.Equal(["serialize@example.com"], received.Mail.Recipients); + Assert.Equal("Serialization Test", received.Mail.Subject); + Assert.Equal("Bold", received.Mail.Body); + Assert.True(received.Mail.IsHtml); } ///