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)
This commit is contained in:
@@ -25,34 +25,12 @@ public record CreateEmailAccountCommand : IRequest<int>
|
||||
public bool IsActive { get; init; } = true;
|
||||
}
|
||||
|
||||
public class CreateEmailAccountCommandHandler(IUnitOfWork unitOfWork)
|
||||
public class CreateEmailAccountCommandHandler(IRepository<EmailAccount> repository)
|
||||
: IRequestHandler<CreateEmailAccountCommand, int>
|
||||
{
|
||||
public async Task<int> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get email account by ID.
|
||||
/// </summary>
|
||||
public record GetEmailAccountByIdQuery(int Id) : IRequest<EmailAccountDto?>;
|
||||
|
||||
public class GetEmailAccountByIdQueryHandler(
|
||||
IRepository<EmailAccount> repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetEmailAccountByIdQuery, EmailAccountDto?>
|
||||
{
|
||||
public async Task<EmailAccountDto?> Handle(
|
||||
GetEmailAccountByIdQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var account = await repository.GetByIdAsync(request.Id, cancellationToken);
|
||||
return account != null ? mapper.Map<EmailAccountDto>(account) : null;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get all email accounts.
|
||||
/// </summary>
|
||||
public record GetEmailAccountsQuery : IRequest<IEnumerable<EmailAccountDto>>;
|
||||
|
||||
public class GetEmailAccountsQueryHandler(
|
||||
IRepository<EmailAccount> repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetEmailAccountsQuery, IEnumerable<EmailAccountDto>>
|
||||
{
|
||||
public async Task<IEnumerable<EmailAccountDto>> Handle(
|
||||
GetEmailAccountsQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var accounts = await repository.GetAllAsync(cancellationToken);
|
||||
return mapper.Map<IEnumerable<EmailAccountDto>>(accounts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using DigitalData.EmailProfiler.Application.Features.EmailAccounts.Commands;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.Features.EmailAccounts.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for CreateEmailAccountCommand.
|
||||
/// </summary>
|
||||
public class CreateEmailAccountCommandValidator : AbstractValidator<CreateEmailAccountCommand>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get email history by ID with attachments.
|
||||
/// </summary>
|
||||
public record GetEmailHistoryByIdQuery(int Id) : IRequest<EmailHistoryDto?>;
|
||||
|
||||
public class GetEmailHistoryByIdQueryHandler(
|
||||
IEmailHistoryRepository repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetEmailHistoryByIdQuery, EmailHistoryDto?>
|
||||
{
|
||||
public async Task<EmailHistoryDto?> Handle(
|
||||
GetEmailHistoryByIdQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var history = await repository.GetWithAttachmentsAsync(request.Id, cancellationToken);
|
||||
return history != null ? mapper.Map<EmailHistoryDto>(history) : null;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get email history by profile with pagination.
|
||||
/// </summary>
|
||||
public record GetEmailHistoryByProfileQuery(
|
||||
int ProfileId,
|
||||
int PageNumber = 1,
|
||||
int PageSize = 50) : IRequest<EmailHistoryPagedResult>;
|
||||
|
||||
/// <summary>
|
||||
/// Paged result for email history.
|
||||
/// </summary>
|
||||
public record EmailHistoryPagedResult(
|
||||
IEnumerable<EmailHistoryDto> Items,
|
||||
int TotalCount,
|
||||
int PageNumber,
|
||||
int PageSize);
|
||||
|
||||
public class GetEmailHistoryByProfileQueryHandler(
|
||||
IEmailHistoryRepository repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetEmailHistoryByProfileQuery, EmailHistoryPagedResult>
|
||||
{
|
||||
public async Task<EmailHistoryPagedResult> Handle(
|
||||
GetEmailHistoryByProfileQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var (items, totalCount) = await repository.GetByProfileIdAsync(
|
||||
request.ProfileId,
|
||||
request.PageNumber,
|
||||
request.PageSize,
|
||||
cancellationToken);
|
||||
|
||||
var dtos = mapper.Map<IEnumerable<EmailHistoryDto>>(items);
|
||||
|
||||
return new EmailHistoryPagedResult(
|
||||
dtos,
|
||||
totalCount,
|
||||
request.PageNumber,
|
||||
request.PageSize);
|
||||
}
|
||||
}
|
||||
@@ -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<EmailAttachment> attachmentRepository,
|
||||
IPublisher publisher,
|
||||
MessageIdGenerator messageIdGenerator,
|
||||
IPdfProcessingService pdfService,
|
||||
@@ -45,7 +49,7 @@ public class ProcessEmailCommandHandler(
|
||||
public async Task<int> 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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using DigitalData.EmailProfiler.Application.Features.EmailProcessing.Commands;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.Features.EmailProcessing.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for ProcessEmailCommand.
|
||||
/// </summary>
|
||||
public class ProcessEmailCommandValidator : AbstractValidator<ProcessEmailCommand>
|
||||
{
|
||||
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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -17,26 +17,12 @@ public record CreateEmailProfileCommand : IRequest<int>
|
||||
public bool IsActive { get; init; } = true;
|
||||
}
|
||||
|
||||
public class CreateEmailProfileCommandHandler(IUnitOfWork unitOfWork)
|
||||
public class CreateEmailProfileCommandHandler(IRepository<EmailProfile> repository)
|
||||
: IRequestHandler<CreateEmailProfileCommand, int>
|
||||
{
|
||||
public async Task<int> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// Command to delete an email profile.
|
||||
/// </summary>
|
||||
public record DeleteEmailProfileCommand(int Id) : IRequest<Unit>;
|
||||
public record DeleteEmailProfileCommand(int Id) : IRequest<int>;
|
||||
|
||||
public class DeleteEmailProfileCommandHandler(IUnitOfWork unitOfWork)
|
||||
: IRequestHandler<DeleteEmailProfileCommand, Unit>
|
||||
public class DeleteEmailProfileCommandHandler(IRepository<EmailProfile> repository)
|
||||
: IRequestHandler<DeleteEmailProfileCommand, int>
|
||||
{
|
||||
public async Task<Unit> Handle(DeleteEmailProfileCommand request, CancellationToken cancellationToken)
|
||||
public async Task<int> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// Command to update an existing email profile.
|
||||
/// </summary>
|
||||
public record UpdateEmailProfileCommand : IRequest<Unit>
|
||||
public record UpdateEmailProfileCommand : IRequest<int>
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string ProfileName { get; init; } = string.Empty;
|
||||
@@ -16,24 +16,12 @@ public record UpdateEmailProfileCommand : IRequest<Unit>
|
||||
public bool IsActive { get; init; }
|
||||
}
|
||||
|
||||
public class UpdateEmailProfileCommandHandler(IUnitOfWork unitOfWork)
|
||||
: IRequestHandler<UpdateEmailProfileCommand, Unit>
|
||||
public class UpdateEmailProfileCommandHandler(IRepository<EmailProfile> repository)
|
||||
: IRequestHandler<UpdateEmailProfileCommand, int>
|
||||
{
|
||||
public async Task<Unit> Handle(UpdateEmailProfileCommand request, CancellationToken cancellationToken)
|
||||
public async Task<int> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get all active email profiles.
|
||||
/// </summary>
|
||||
public record GetActiveEmailProfilesQuery : IRequest<IEnumerable<EmailProfileDto>>;
|
||||
|
||||
public class GetActiveEmailProfilesQueryHandler(
|
||||
IEmailProfileRepository repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetActiveEmailProfilesQuery, IEnumerable<EmailProfileDto>>
|
||||
{
|
||||
public async Task<IEnumerable<EmailProfileDto>> Handle(
|
||||
GetActiveEmailProfilesQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var profiles = await repository.GetActiveProfilesAsync(cancellationToken);
|
||||
return mapper.Map<IEnumerable<EmailProfileDto>>(profiles);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get email profile by ID with related entities.
|
||||
/// </summary>
|
||||
public record GetEmailProfileByIdQuery(int Id) : IRequest<EmailProfileDto?>;
|
||||
|
||||
public class GetEmailProfileByIdQueryHandler(
|
||||
IEmailProfileRepository repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetEmailProfileByIdQuery, EmailProfileDto?>
|
||||
{
|
||||
public async Task<EmailProfileDto?> Handle(
|
||||
GetEmailProfileByIdQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var profile = await repository.GetWithRelatedEntitiesAsync(request.Id, cancellationToken);
|
||||
return profile != null ? mapper.Map<EmailProfileDto>(profile) : null;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Query to get all email profiles.
|
||||
/// </summary>
|
||||
public record GetEmailProfilesQuery : IRequest<IEnumerable<EmailProfileDto>>;
|
||||
|
||||
public class GetEmailProfilesQueryHandler(
|
||||
IRepository<EmailProfile> repository,
|
||||
IMapper mapper)
|
||||
: IRequestHandler<GetEmailProfilesQuery, IEnumerable<EmailProfileDto>>
|
||||
{
|
||||
public async Task<IEnumerable<EmailProfileDto>> Handle(
|
||||
GetEmailProfilesQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var profiles = await repository.GetAllAsync(cancellationToken);
|
||||
return mapper.Map<IEnumerable<EmailProfileDto>>(profiles);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for CreateEmailProfileCommand.
|
||||
/// </summary>
|
||||
public class CreateEmailProfileCommandValidator : AbstractValidator<CreateEmailProfileCommand>
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DigitalData.EmailProfiler.Application.Features.EmailProfiles.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for UpdateEmailProfileCommand.
|
||||
/// </summary>
|
||||
public class UpdateEmailProfileCommandValidator : AbstractValidator<UpdateEmailProfileCommand>
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user