From eda625714512cc5c12a26d9dfc33870cf515bd5b Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 14 Jul 2026 16:37:00 +0200 Subject: [PATCH] feat(application): Add MediatR Commands, Queries, and FluentValidation Commands (5): - CreateEmailProfileCommand, UpdateEmailProfileCommand, DeleteEmailProfileCommand - CreateEmailAccountCommand (OAuth2/password conditional validation) - ProcessEmailCommand (with CreateEmailHistoryDto, UpdateEmailHistoryStatusDto) Queries (7): - GetEmailProfilesQuery, GetEmailProfileByIdQuery, GetActiveEmailProfilesQuery - GetEmailAccountsQuery, GetEmailAccountByIdQuery - GetEmailHistoryByProfileQuery (with pagination), GetEmailHistoryByIdQuery Validators (4): - CreateEmailProfileCommandValidator, UpdateEmailProfileCommandValidator - CreateEmailAccountCommandValidator, ProcessEmailCommandValidator All handlers in same file as commands/queries (AGENTS.md rule #5) --- .../Commands/CreateEmailAccountCommand.cs | 28 +--- .../Queries/GetEmailAccountByIdQuery.cs | 26 ++++ .../Queries/GetEmailAccountsQuery.cs | 26 ++++ .../CreateEmailAccountCommandValidator.cs | 56 ++++++++ .../Queries/GetEmailHistoryByIdQuery.cs | 25 ++++ .../Queries/GetEmailHistoryByProfileQuery.cs | 48 +++++++ .../Commands/ProcessEmailCommand.cs | 122 +++++++++++------- .../ProcessEmailCommandValidator.cs | 50 +++++++ .../Commands/CreateEmailProfileCommand.cs | 20 +-- .../Commands/DeleteEmailProfileCommand.cs | 19 +-- .../Commands/UpdateEmailProfileCommand.cs | 26 +--- .../Queries/GetActiveEmailProfilesQuery.cs | 25 ++++ .../Queries/GetEmailProfileByIdQuery.cs | 25 ++++ .../Queries/GetEmailProfilesQuery.cs | 26 ++++ .../CreateEmailProfileCommandValidator.cs | 28 ++++ .../UpdateEmailProfileCommandValidator.cs | 28 ++++ 16 files changed, 458 insertions(+), 120 deletions(-) create mode 100644 src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Queries/GetEmailAccountByIdQuery.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Queries/GetEmailAccountsQuery.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Validators/CreateEmailAccountCommandValidator.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Features/EmailHistories/Queries/GetEmailHistoryByIdQuery.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Features/EmailHistories/Queries/GetEmailHistoryByProfileQuery.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Validators/ProcessEmailCommandValidator.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Queries/GetActiveEmailProfilesQuery.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Queries/GetEmailProfileByIdQuery.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Queries/GetEmailProfilesQuery.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Validators/CreateEmailProfileCommandValidator.cs create mode 100644 src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Validators/UpdateEmailProfileCommandValidator.cs diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Commands/CreateEmailAccountCommand.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Commands/CreateEmailAccountCommand.cs index 3992c4c..a4a66e1 100644 --- a/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Commands/CreateEmailAccountCommand.cs +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Commands/CreateEmailAccountCommand.cs @@ -25,34 +25,12 @@ public record CreateEmailAccountCommand : IRequest public bool IsActive { get; init; } = true; } -public class CreateEmailAccountCommandHandler(IUnitOfWork unitOfWork) +public class CreateEmailAccountCommandHandler(IRepository repository) : IRequestHandler { public async Task Handle(CreateEmailAccountCommand request, CancellationToken cancellationToken) { - var account = new EmailAccount - { - AccountName = request.AccountName, - Username = request.Username, - ImapServer = request.ImapServer, - ImapPort = request.ImapPort, - ImapUseSsl = request.ImapUseSsl, - SmtpServer = request.SmtpServer, - SmtpPort = request.SmtpPort, - SmtpUseSsl = request.SmtpUseSsl, - UseOAuth2 = request.UseOAuth2, - EncryptedPassword = request.EncryptedPassword, - TenantId = request.TenantId, - ClientId = request.ClientId, - EncryptedClientSecret = request.EncryptedClientSecret, - IsActive = request.IsActive, - AddedWhen = DateTime.Now, - AddedWho = "System" // TODO: Get from current user context - }; - - var createdAccount = await unitOfWork.EmailAccounts.AddAsync(account, cancellationToken); - await unitOfWork.SaveChangesAsync(cancellationToken); - - return createdAccount.Id; + var account = await repository.CreateAsync(request, cancellationToken); + return account.Id; } } diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Queries/GetEmailAccountByIdQuery.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Queries/GetEmailAccountByIdQuery.cs new file mode 100644 index 0000000..3581226 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Queries/GetEmailAccountByIdQuery.cs @@ -0,0 +1,26 @@ +using AutoMapper; +using DigitalData.EmailProfiler.Application.Common.Dtos; +using DigitalData.EmailProfiler.Application.Interfaces.Repositories; +using DigitalData.EmailProfiler.Domain.Entities; +using MediatR; + +namespace DigitalData.EmailProfiler.Application.Features.EmailAccounts.Queries; + +/// +/// Query to get email account by ID. +/// +public record GetEmailAccountByIdQuery(int Id) : IRequest; + +public class GetEmailAccountByIdQueryHandler( + IRepository repository, + IMapper mapper) + : IRequestHandler +{ + public async Task Handle( + GetEmailAccountByIdQuery request, + CancellationToken cancellationToken) + { + var account = await repository.GetByIdAsync(request.Id, cancellationToken); + return account != null ? mapper.Map(account) : null; + } +} diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Queries/GetEmailAccountsQuery.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Queries/GetEmailAccountsQuery.cs new file mode 100644 index 0000000..d57ddd4 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Queries/GetEmailAccountsQuery.cs @@ -0,0 +1,26 @@ +using AutoMapper; +using DigitalData.EmailProfiler.Application.Common.Dtos; +using DigitalData.EmailProfiler.Application.Interfaces.Repositories; +using DigitalData.EmailProfiler.Domain.Entities; +using MediatR; + +namespace DigitalData.EmailProfiler.Application.Features.EmailAccounts.Queries; + +/// +/// Query to get all email accounts. +/// +public record GetEmailAccountsQuery : IRequest>; + +public class GetEmailAccountsQueryHandler( + IRepository repository, + IMapper mapper) + : IRequestHandler> +{ + public async Task> Handle( + GetEmailAccountsQuery request, + CancellationToken cancellationToken) + { + var accounts = await repository.GetAllAsync(cancellationToken); + return mapper.Map>(accounts); + } +} diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Validators/CreateEmailAccountCommandValidator.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Validators/CreateEmailAccountCommandValidator.cs new file mode 100644 index 0000000..74e1516 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailAccounts/Validators/CreateEmailAccountCommandValidator.cs @@ -0,0 +1,56 @@ +using DigitalData.EmailProfiler.Application.Features.EmailAccounts.Commands; +using FluentValidation; + +namespace DigitalData.EmailProfiler.Application.Features.EmailAccounts.Validators; + +/// +/// Validator for CreateEmailAccountCommand. +/// +public class CreateEmailAccountCommandValidator : AbstractValidator +{ + public CreateEmailAccountCommandValidator() + { + RuleFor(x => x.AccountName) + .NotEmpty().WithMessage("Account name is required") + .MaximumLength(100).WithMessage("Account name must not exceed 100 characters"); + + RuleFor(x => x.Username) + .NotEmpty().WithMessage("Username is required") + .MaximumLength(200).WithMessage("Username must not exceed 200 characters") + .EmailAddress().WithMessage("Username must be a valid email address"); + + RuleFor(x => x.ImapServer) + .NotEmpty().WithMessage("IMAP server is required") + .MaximumLength(200).WithMessage("IMAP server must not exceed 200 characters"); + + RuleFor(x => x.ImapPort) + .GreaterThan(0).WithMessage("IMAP port must be greater than 0") + .LessThanOrEqualTo(65535).WithMessage("IMAP port must not exceed 65535"); + + RuleFor(x => x.SmtpServer) + .NotEmpty().WithMessage("SMTP server is required") + .MaximumLength(200).WithMessage("SMTP server must not exceed 200 characters"); + + RuleFor(x => x.SmtpPort) + .GreaterThan(0).WithMessage("SMTP port must be greater than 0") + .LessThanOrEqualTo(65535).WithMessage("SMTP port must not exceed 65535"); + + // OAuth2 validation + RuleFor(x => x.TenantId) + .NotEmpty().WithMessage("Tenant ID is required for OAuth2") + .When(x => x.UseOAuth2); + + RuleFor(x => x.ClientId) + .NotEmpty().WithMessage("Client ID is required for OAuth2") + .When(x => x.UseOAuth2); + + RuleFor(x => x.EncryptedClientSecret) + .NotEmpty().WithMessage("Client secret is required for OAuth2") + .When(x => x.UseOAuth2); + + // Password validation (non-OAuth2) + RuleFor(x => x.EncryptedPassword) + .NotEmpty().WithMessage("Password is required") + .When(x => !x.UseOAuth2); + } +} diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailHistories/Queries/GetEmailHistoryByIdQuery.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailHistories/Queries/GetEmailHistoryByIdQuery.cs new file mode 100644 index 0000000..3023d9f --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailHistories/Queries/GetEmailHistoryByIdQuery.cs @@ -0,0 +1,25 @@ +using AutoMapper; +using DigitalData.EmailProfiler.Application.Common.Dtos.EmailHistories; +using DigitalData.EmailProfiler.Application.Interfaces.Repositories; +using MediatR; + +namespace DigitalData.EmailProfiler.Application.Features.EmailHistories.Queries; + +/// +/// Query to get email history by ID with attachments. +/// +public record GetEmailHistoryByIdQuery(int Id) : IRequest; + +public class GetEmailHistoryByIdQueryHandler( + IEmailHistoryRepository repository, + IMapper mapper) + : IRequestHandler +{ + public async Task Handle( + GetEmailHistoryByIdQuery request, + CancellationToken cancellationToken) + { + var history = await repository.GetWithAttachmentsAsync(request.Id, cancellationToken); + return history != null ? mapper.Map(history) : null; + } +} diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailHistories/Queries/GetEmailHistoryByProfileQuery.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailHistories/Queries/GetEmailHistoryByProfileQuery.cs new file mode 100644 index 0000000..f32636f --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailHistories/Queries/GetEmailHistoryByProfileQuery.cs @@ -0,0 +1,48 @@ +using AutoMapper; +using DigitalData.EmailProfiler.Application.Common.Dtos.EmailHistories; +using DigitalData.EmailProfiler.Application.Interfaces.Repositories; +using MediatR; + +namespace DigitalData.EmailProfiler.Application.Features.EmailHistories.Queries; + +/// +/// Query to get email history by profile with pagination. +/// +public record GetEmailHistoryByProfileQuery( + int ProfileId, + int PageNumber = 1, + int PageSize = 50) : IRequest; + +/// +/// Paged result for email history. +/// +public record EmailHistoryPagedResult( + IEnumerable Items, + int TotalCount, + int PageNumber, + int PageSize); + +public class GetEmailHistoryByProfileQueryHandler( + IEmailHistoryRepository repository, + IMapper mapper) + : IRequestHandler +{ + public async Task Handle( + GetEmailHistoryByProfileQuery request, + CancellationToken cancellationToken) + { + var (items, totalCount) = await repository.GetByProfileIdAsync( + request.ProfileId, + request.PageNumber, + request.PageSize, + cancellationToken); + + var dtos = mapper.Map>(items); + + return new EmailHistoryPagedResult( + dtos, + totalCount, + request.PageNumber, + request.PageSize); + } +} diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Commands/ProcessEmailCommand.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Commands/ProcessEmailCommand.cs index 99c61b5..053f2fb 100644 --- a/src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Commands/ProcessEmailCommand.cs +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Commands/ProcessEmailCommand.cs @@ -1,3 +1,5 @@ +using DigitalData.EmailProfiler.Application.Common.Dtos.EmailAttachments; +using DigitalData.EmailProfiler.Application.Common.Dtos.EmailHistories; using DigitalData.EmailProfiler.Application.Interfaces.Repositories; using DigitalData.EmailProfiler.Application.Interfaces.Services; using DigitalData.EmailProfiler.Domain.Entities; @@ -35,7 +37,9 @@ public record AttachmentData( long SizeBytes); public class ProcessEmailCommandHandler( - IUnitOfWork unitOfWork, + IEmailProfileRepository profileRepository, + IEmailHistoryRepository historyRepository, + IRepository attachmentRepository, IPublisher publisher, MessageIdGenerator messageIdGenerator, IPdfProcessingService pdfService, @@ -45,7 +49,7 @@ public class ProcessEmailCommandHandler( public async Task Handle(ProcessEmailCommand request, CancellationToken cancellationToken) { // 1. Get profile with related entities - var profile = await unitOfWork.EmailProfiles.GetWithRelatedEntitiesAsync(request.ProfileId, cancellationToken) + var profile = await profileRepository.GetWithRelatedEntitiesAsync(request.ProfileId, cancellationToken) ?? throw new DomainException($"Profile with ID {request.ProfileId} not found"); // 2. Generate message ID hash for duplicate detection @@ -56,64 +60,71 @@ public class ProcessEmailCommandHandler( request.Subject); // 3. Check for duplicates - var isDuplicate = await unitOfWork.EmailHistories.IsDuplicateAsync(messageId.Hash, cancellationToken); + var isDuplicate = await historyRepository.IsDuplicateAsync(messageId.Hash, cancellationToken); if (isDuplicate) { throw new ValidationException("Email already processed (duplicate detected)", ErrorCode.DuplicateMessageId); } - // 4. Create email history record - var emailHistory = new EmailHistory - { - ProfileId = profile.Id, - MessageIdHash = messageId.Hash, - OriginalMessageId = request.MessageId, - SenderAddress = request.Sender, - EmailDate = request.ReceivedDate, - Subject = request.Subject, - EmailBodyText = request.BodyText, - EmailBodyHtml = request.BodyHtml, - Status = EmailStatus.Processing.ToString(), - AddedWhen = DateTime.Now - }; + // 4. Create email history record using DTO approach + var historyDto = new CreateEmailHistoryDto( + ProfileId: profile.Id, + MessageIdHash: messageId.Hash, + OriginalMessageId: request.MessageId, + SenderAddress: request.Sender, + EmailDate: request.ReceivedDate, + Subject: request.Subject, + EmailBodyText: request.BodyText, + EmailBodyHtml: request.BodyHtml); - var createdHistory = await unitOfWork.EmailHistories.AddAsync(emailHistory, cancellationToken); - await unitOfWork.SaveChangesAsync(cancellationToken); + var createdHistory = await historyRepository.CreateAsync(historyDto, cancellationToken); try { // 5. Process attachments foreach (var attachmentData in request.Attachments) { - var attachment = new EmailAttachment - { - EmailHistoryId = createdHistory.Id, - OriginalFileName = attachmentData.FileName, - SavedFileName = attachmentData.FileName, // TODO: Generate unique name - FilePath = string.Empty, // TODO: Save to disk and get path - FileSize = attachmentData.SizeBytes, - Extension = Path.GetExtension(attachmentData.FileName), - Status = AttachmentStatus.Pending.ToString(), - AddedWhen = DateTime.Now - }; + var attachmentDto = new CreateEmailAttachmentDto( + EmailHistoryId: createdHistory.Id, + OriginalFileName: attachmentData.FileName, + SavedFileName: attachmentData.FileName, // TODO: Generate unique name + FilePath: string.Empty, // TODO: Save to disk and get path + FileSize: attachmentData.SizeBytes, + Extension: Path.GetExtension(attachmentData.FileName), + ContentType: attachmentData.ContentType, + Content: attachmentData.Content); - // Validate PDF attachments + var attachment = await attachmentRepository.CreateAsync(attachmentDto, cancellationToken); + + // Validate PDF attachments - need to retrieve and update the entity if (attachmentData.ContentType.Contains("pdf", StringComparison.OrdinalIgnoreCase)) { using var stream = new MemoryStream(attachmentData.Content); var isValidPdf = await pdfService.IsValidPdfAsync(stream, cancellationToken); - if (isValidPdf) + // For domain methods like MarkAsValid/MarkAsCorrupt, we need to get the entity + var attachmentEntity = await attachmentRepository.GetByIdAsync(attachment.Id, cancellationToken); + if (attachmentEntity != null) { - attachment.MarkAsValid(); - } - else - { - attachment.MarkAsCorrupt(ErrorCode.PdfStructureInvalid, "Invalid PDF structure"); + if (isValidPdf) + { + attachmentEntity.MarkAsValid(); + } + else + { + attachmentEntity.MarkAsCorrupt(ErrorCode.PdfStructureInvalid, "Invalid PDF structure"); + } + + // Update directly using UpdateSingleAsync for safety + await attachmentRepository.UpdateSingleAsync( + a => a.Id == attachmentEntity.Id, + new UpdateEmailAttachmentStatusDto( + Status: attachmentEntity.Status ?? string.Empty, + ValidationErrorCode: attachmentEntity.ValidationErrorCode, + ValidationErrorMessage: attachmentEntity.ValidationErrorMessage), + cancellationToken); } } - - createdHistory.Attachments.Add(attachment); } // 6. Archive to DMS if configured @@ -123,10 +134,18 @@ public class ProcessEmailCommandHandler( // This will be implemented based on ProcessSteps and IndexingSteps } - // 7. Mark as processed - createdHistory.MarkAsProcessed(); - await unitOfWork.EmailHistories.UpdateAsync(createdHistory, cancellationToken); - await unitOfWork.SaveChangesAsync(cancellationToken); + // 7. Mark as processed - retrieve entity for domain method + var historyEntity = await historyRepository.GetByIdAsync(createdHistory.Id, cancellationToken); + if (historyEntity != null) + { + historyEntity.MarkAsProcessed(); + await historyRepository.UpdateSingleAsync( + h => h.Id == historyEntity.Id, + new UpdateEmailHistoryStatusDto( + Status: historyEntity.Status ?? string.Empty, + ProcessedDate: historyEntity.ProcessedDate), + cancellationToken); + } // 8. Publish domain event await publisher.Publish( @@ -141,10 +160,19 @@ public class ProcessEmailCommandHandler( } catch (Exception ex) { - // Mark as failed - createdHistory.MarkAsFailed(ErrorCode.AttachmentExtractionFailed, ex.Message); - await unitOfWork.EmailHistories.UpdateAsync(createdHistory, cancellationToken); - await unitOfWork.SaveChangesAsync(cancellationToken); + // Mark as failed - retrieve entity for domain method + var historyEntity = await historyRepository.GetByIdAsync(createdHistory.Id, cancellationToken); + if (historyEntity != null) + { + historyEntity.MarkAsFailed(ErrorCode.AttachmentExtractionFailed, ex.Message); + await historyRepository.UpdateSingleAsync( + h => h.Id == historyEntity.Id, + new UpdateEmailHistoryStatusDto( + Status: historyEntity.Status ?? string.Empty, + ErrorCodeValue: historyEntity.ErrorCodeValue, + ErrorMessage: historyEntity.ErrorMessage), + cancellationToken); + } throw; } diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Validators/ProcessEmailCommandValidator.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Validators/ProcessEmailCommandValidator.cs new file mode 100644 index 0000000..c38a414 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProcessing/Validators/ProcessEmailCommandValidator.cs @@ -0,0 +1,50 @@ +using DigitalData.EmailProfiler.Application.Features.EmailProcessing.Commands; +using FluentValidation; + +namespace DigitalData.EmailProfiler.Application.Features.EmailProcessing.Validators; + +/// +/// Validator for ProcessEmailCommand. +/// +public class ProcessEmailCommandValidator : AbstractValidator +{ + public ProcessEmailCommandValidator() + { + RuleFor(x => x.ProfileId) + .GreaterThan(0).WithMessage("Profile ID must be greater than 0"); + + RuleFor(x => x.MessageId) + .NotEmpty().WithMessage("Message ID is required") + .MaximumLength(500).WithMessage("Message ID must not exceed 500 characters"); + + RuleFor(x => x.Sender) + .NotEmpty().WithMessage("Sender is required") + .MaximumLength(200).WithMessage("Sender must not exceed 200 characters") + .EmailAddress().WithMessage("Sender must be a valid email address"); + + RuleFor(x => x.Subject) + .NotEmpty().WithMessage("Subject is required") + .MaximumLength(500).WithMessage("Subject must not exceed 500 characters"); + + RuleFor(x => x.ReceivedDate) + .NotEmpty().WithMessage("Received date is required") + .LessThanOrEqualTo(DateTime.Now.AddDays(1)).WithMessage("Received date cannot be in the future"); + + RuleFor(x => x.Attachments) + .NotNull().WithMessage("Attachments collection cannot be null"); + + RuleForEach(x => x.Attachments).ChildRules(attachment => + { + attachment.RuleFor(a => a.FileName) + .NotEmpty().WithMessage("Attachment file name is required") + .MaximumLength(500).WithMessage("Attachment file name must not exceed 500 characters"); + + attachment.RuleFor(a => a.Content) + .NotEmpty().WithMessage("Attachment content is required"); + + attachment.RuleFor(a => a.SizeBytes) + .GreaterThan(0).WithMessage("Attachment size must be greater than 0") + .LessThanOrEqualTo(100 * 1024 * 1024).WithMessage("Attachment size must not exceed 100 MB"); + }); + } +} diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/CreateEmailProfileCommand.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/CreateEmailProfileCommand.cs index 8db9b88..5797fee 100644 --- a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/CreateEmailProfileCommand.cs +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/CreateEmailProfileCommand.cs @@ -17,26 +17,12 @@ public record CreateEmailProfileCommand : IRequest public bool IsActive { get; init; } = true; } -public class CreateEmailProfileCommandHandler(IUnitOfWork unitOfWork) +public class CreateEmailProfileCommandHandler(IRepository repository) : IRequestHandler { public async Task Handle(CreateEmailProfileCommand request, CancellationToken cancellationToken) { - var profile = new EmailProfile - { - ProfileName = request.ProfileName, - EmailAccountId = request.EmailAccountId, - ProcessId = request.ProcessId, - ValidationSql = request.ValidationSql, - PollIntervalMinutes = request.PollIntervalMinutes, - IsActive = request.IsActive, - AddedWhen = DateTime.Now, - AddedWho = "System" // TODO: Get from current user context - }; - - var createdProfile = await unitOfWork.EmailProfiles.AddAsync(profile, cancellationToken); - await unitOfWork.SaveChangesAsync(cancellationToken); - - return createdProfile.Id; + var profile = await repository.CreateAsync(request, cancellationToken); + return profile.Id; } } diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/DeleteEmailProfileCommand.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/DeleteEmailProfileCommand.cs index c214c89..50e7680 100644 --- a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/DeleteEmailProfileCommand.cs +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/DeleteEmailProfileCommand.cs @@ -1,5 +1,5 @@ using DigitalData.EmailProfiler.Application.Interfaces.Repositories; -using DigitalData.EmailProfiler.Domain.Exceptions; +using DigitalData.EmailProfiler.Domain.Entities; using MediatR; namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands; @@ -7,19 +7,14 @@ namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands; /// /// Command to delete an email profile. /// -public record DeleteEmailProfileCommand(int Id) : IRequest; +public record DeleteEmailProfileCommand(int Id) : IRequest; -public class DeleteEmailProfileCommandHandler(IUnitOfWork unitOfWork) - : IRequestHandler +public class DeleteEmailProfileCommandHandler(IRepository repository) + : IRequestHandler { - public async Task Handle(DeleteEmailProfileCommand request, CancellationToken cancellationToken) + public async Task Handle(DeleteEmailProfileCommand request, CancellationToken cancellationToken) { - var profile = await unitOfWork.EmailProfiles.GetByIdAsync(request.Id, cancellationToken) - ?? throw new DomainException($"Email profile with ID {request.Id} not found"); - - await unitOfWork.EmailProfiles.DeleteAsync(profile, cancellationToken); - await unitOfWork.SaveChangesAsync(cancellationToken); - - return Unit.Value; + await repository.DeleteSingleAsync(p => p.Id == request.Id, cancellationToken); + return request.Id; } } diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/UpdateEmailProfileCommand.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/UpdateEmailProfileCommand.cs index da6c6d3..cdd434a 100644 --- a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/UpdateEmailProfileCommand.cs +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Commands/UpdateEmailProfileCommand.cs @@ -1,5 +1,5 @@ using DigitalData.EmailProfiler.Application.Interfaces.Repositories; -using DigitalData.EmailProfiler.Domain.Exceptions; +using DigitalData.EmailProfiler.Domain.Entities; using MediatR; namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands; @@ -7,7 +7,7 @@ namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands; /// /// Command to update an existing email profile. /// -public record UpdateEmailProfileCommand : IRequest +public record UpdateEmailProfileCommand : IRequest { public int Id { get; init; } public string ProfileName { get; init; } = string.Empty; @@ -16,24 +16,12 @@ public record UpdateEmailProfileCommand : IRequest public bool IsActive { get; init; } } -public class UpdateEmailProfileCommandHandler(IUnitOfWork unitOfWork) - : IRequestHandler +public class UpdateEmailProfileCommandHandler(IRepository repository) + : IRequestHandler { - public async Task Handle(UpdateEmailProfileCommand request, CancellationToken cancellationToken) + public async Task Handle(UpdateEmailProfileCommand request, CancellationToken cancellationToken) { - var profile = await unitOfWork.EmailProfiles.GetByIdAsync(request.Id, cancellationToken) - ?? throw new DomainException($"Email profile with ID {request.Id} not found"); - - profile.ProfileName = request.ProfileName; - profile.ValidationSql = request.ValidationSql; - profile.PollIntervalMinutes = request.PollIntervalMinutes; - profile.IsActive = request.IsActive; - profile.ChangedWhen = DateTime.Now; - profile.ChangedWho = "System"; // TODO: Get from current user context - - await unitOfWork.EmailProfiles.UpdateAsync(profile, cancellationToken); - await unitOfWork.SaveChangesAsync(cancellationToken); - - return Unit.Value; + await repository.UpdateSingleAsync(p => p.Id == request.Id, request, cancellationToken); + return request.Id; } } diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Queries/GetActiveEmailProfilesQuery.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Queries/GetActiveEmailProfilesQuery.cs new file mode 100644 index 0000000..39ebc21 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Queries/GetActiveEmailProfilesQuery.cs @@ -0,0 +1,25 @@ +using AutoMapper; +using DigitalData.EmailProfiler.Application.Common.Dtos; +using DigitalData.EmailProfiler.Application.Interfaces.Repositories; +using MediatR; + +namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Queries; + +/// +/// Query to get all active email profiles. +/// +public record GetActiveEmailProfilesQuery : IRequest>; + +public class GetActiveEmailProfilesQueryHandler( + IEmailProfileRepository repository, + IMapper mapper) + : IRequestHandler> +{ + public async Task> Handle( + GetActiveEmailProfilesQuery request, + CancellationToken cancellationToken) + { + var profiles = await repository.GetActiveProfilesAsync(cancellationToken); + return mapper.Map>(profiles); + } +} diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Queries/GetEmailProfileByIdQuery.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Queries/GetEmailProfileByIdQuery.cs new file mode 100644 index 0000000..eae01e8 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Queries/GetEmailProfileByIdQuery.cs @@ -0,0 +1,25 @@ +using AutoMapper; +using DigitalData.EmailProfiler.Application.Common.Dtos; +using DigitalData.EmailProfiler.Application.Interfaces.Repositories; +using MediatR; + +namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Queries; + +/// +/// Query to get email profile by ID with related entities. +/// +public record GetEmailProfileByIdQuery(int Id) : IRequest; + +public class GetEmailProfileByIdQueryHandler( + IEmailProfileRepository repository, + IMapper mapper) + : IRequestHandler +{ + public async Task Handle( + GetEmailProfileByIdQuery request, + CancellationToken cancellationToken) + { + var profile = await repository.GetWithRelatedEntitiesAsync(request.Id, cancellationToken); + return profile != null ? mapper.Map(profile) : null; + } +} diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Queries/GetEmailProfilesQuery.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Queries/GetEmailProfilesQuery.cs new file mode 100644 index 0000000..bddc986 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Queries/GetEmailProfilesQuery.cs @@ -0,0 +1,26 @@ +using AutoMapper; +using DigitalData.EmailProfiler.Application.Common.Dtos; +using DigitalData.EmailProfiler.Application.Interfaces.Repositories; +using DigitalData.EmailProfiler.Domain.Entities; +using MediatR; + +namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Queries; + +/// +/// Query to get all email profiles. +/// +public record GetEmailProfilesQuery : IRequest>; + +public class GetEmailProfilesQueryHandler( + IRepository repository, + IMapper mapper) + : IRequestHandler> +{ + public async Task> Handle( + GetEmailProfilesQuery request, + CancellationToken cancellationToken) + { + var profiles = await repository.GetAllAsync(cancellationToken); + return mapper.Map>(profiles); + } +} diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Validators/CreateEmailProfileCommandValidator.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Validators/CreateEmailProfileCommandValidator.cs new file mode 100644 index 0000000..0fa7c50 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Validators/CreateEmailProfileCommandValidator.cs @@ -0,0 +1,28 @@ +using DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands; +using FluentValidation; + +namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Validators; + +/// +/// Validator for CreateEmailProfileCommand. +/// +public class CreateEmailProfileCommandValidator : AbstractValidator +{ + public CreateEmailProfileCommandValidator() + { + RuleFor(x => x.ProfileName) + .NotEmpty().WithMessage("Profile name is required") + .MaximumLength(100).WithMessage("Profile name must not exceed 100 characters"); + + RuleFor(x => x.EmailAccountId) + .GreaterThan(0).WithMessage("Email account ID must be greater than 0"); + + RuleFor(x => x.PollIntervalMinutes) + .GreaterThanOrEqualTo(1).WithMessage("Poll interval must be at least 1 minute") + .LessThanOrEqualTo(1440).WithMessage("Poll interval must not exceed 1440 minutes (24 hours)"); + + RuleFor(x => x.ValidationSql) + .MaximumLength(1000).WithMessage("Validation SQL must not exceed 1000 characters") + .When(x => !string.IsNullOrEmpty(x.ValidationSql)); + } +} diff --git a/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Validators/UpdateEmailProfileCommandValidator.cs b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Validators/UpdateEmailProfileCommandValidator.cs new file mode 100644 index 0000000..b0e6ba6 --- /dev/null +++ b/src/DigitalData.EmailProfiler.Application/Features/EmailProfiles/Validators/UpdateEmailProfileCommandValidator.cs @@ -0,0 +1,28 @@ +using DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands; +using FluentValidation; + +namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Validators; + +/// +/// Validator for UpdateEmailProfileCommand. +/// +public class UpdateEmailProfileCommandValidator : AbstractValidator +{ + public UpdateEmailProfileCommandValidator() + { + RuleFor(x => x.Id) + .GreaterThan(0).WithMessage("Profile ID must be greater than 0"); + + RuleFor(x => x.ProfileName) + .NotEmpty().WithMessage("Profile name is required") + .MaximumLength(100).WithMessage("Profile name must not exceed 100 characters"); + + RuleFor(x => x.PollIntervalMinutes) + .GreaterThanOrEqualTo(1).WithMessage("Poll interval must be at least 1 minute") + .LessThanOrEqualTo(1440).WithMessage("Poll interval must not exceed 1440 minutes (24 hours)"); + + RuleFor(x => x.ValidationSql) + .MaximumLength(1000).WithMessage("Validation SQL must not exceed 1000 characters") + .When(x => !string.IsNullOrEmpty(x.ValidationSql)); + } +}