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:
2026-07-14 16:37:00 +02:00
parent a708799587
commit eda6257145
16 changed files with 458 additions and 120 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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");
});
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}