refactor(application): Remove old CQRS commands/queries for minimal API migration

- Remove EmailAccounts CQRS layer (4 files: commands, queries, validators)
- Remove EmailHistories CQRS layer (2 files: queries)
- Remove EmailProcessing CQRS layer (2 files: commands, validators)
- Remove EmailProfiles CQRS layer (8 files: commands, queries, validators)
- Remove corresponding API controllers (3 files)

Total: 19 files removed

Reason: Migrating from full CQRS pattern to minimal API with direct repository access
Note: SendEmailCommand will be added separately for EmailSenderWorker
This commit is contained in:
2026-07-22 11:49:37 +02:00
parent 3f9bfc78a8
commit d346ed3176
19 changed files with 0 additions and 904 deletions

View File

@@ -1,70 +0,0 @@
using DigitalData.EmailProfiler.Application.Common.Dtos;
using DigitalData.EmailProfiler.Application.Common.Interfaces;
using DigitalData.EmailProfiler.Application.EmailAccounts.Commands;
using DigitalData.EmailProfiler.Application.EmailAccounts.Queries;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace DigitalData.EmailProfiler.API.Controllers;
/// <summary>
/// Email accounts management API controller
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class EmailAccountsController(
IMediator mediator,
ICommandPublisher commandPublisher) : ControllerBase
{
/// <summary>
/// Get all email accounts
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<EmailAccountDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetAll(CancellationToken cancellationToken)
{
var query = new GetEmailAccountsQuery();
var result = await mediator.Send(query, cancellationToken);
return Ok(result);
}
/// <summary>
/// Get email account by ID
/// </summary>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(EmailAccountDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
{
var query = new GetEmailAccountByIdQuery(id);
var result = await mediator.Send(query, cancellationToken);
if (result == null)
return NotFound(new { Message = $"Email account with ID {id} not found" });
return Ok(result);
}
/// <summary>
/// Create new email account (async via RabbitMQ)
/// </summary>
[HttpPost]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(
[FromBody] CreateEmailAccountCommand command,
CancellationToken cancellationToken)
{
// Publish command to RabbitMQ for async processing
await commandPublisher.PublishAsync(command, cancellationToken);
return Accepted(new
{
Message = "Email account creation request queued for processing",
AccountName = command.AccountName
});
}
// Note: Update and Delete operations can be added similarly
// For now, we focus on Create as the main use case
}

View File

@@ -1,57 +0,0 @@
using DigitalData.EmailProfiler.Application.Common.Dtos.EmailHistories;
using DigitalData.EmailProfiler.Application.EmailHistories.Queries;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace DigitalData.EmailProfiler.API.Controllers;
/// <summary>
/// Email history API controller (read-only)
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class EmailHistoryController(IMediator mediator) : ControllerBase
{
/// <summary>
/// Get email history by profile ID with pagination
/// </summary>
[HttpGet("profile/{profileId:int}")]
[ProducesResponseType(typeof(IEnumerable<EmailHistoryDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetByProfile(
int profileId,
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 50,
CancellationToken cancellationToken = default)
{
var query = new GetEmailHistoryByProfileQuery(profileId, pageNumber, pageSize);
var result = await mediator.Send(query, cancellationToken);
return Ok(new
{
ProfileId = profileId,
PageNumber = pageNumber,
PageSize = pageSize,
Data = result
});
}
/// <summary>
/// Get email history by ID
/// </summary>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(EmailHistoryDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
{
var query = new GetEmailHistoryByIdQuery(id);
var result = await mediator.Send(query, cancellationToken);
if (result == null)
return NotFound(new { Message = $"Email history with ID {id} not found" });
return Ok(result);
}
// Note: Email history is typically managed by ProcessEmailCommand
// No direct CREATE/UPDATE/DELETE endpoints needed
}

View File

@@ -1,123 +0,0 @@
using DigitalData.EmailProfiler.Application.Common.Dtos;
using DigitalData.EmailProfiler.Application.Common.Interfaces;
using DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
using DigitalData.EmailProfiler.Application.EmailProfiles.Queries;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace DigitalData.EmailProfiler.API.Controllers;
/// <summary>
/// Email profiles management API controller
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class EmailProfilesController(
IMediator mediator,
ICommandPublisher commandPublisher) : ControllerBase
{
/// <summary>
/// Get all email profiles
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<EmailProfileDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetAll(CancellationToken cancellationToken)
{
var query = new GetEmailProfilesQuery();
var result = await mediator.Send(query, cancellationToken);
return Ok(result);
}
/// <summary>
/// Get email profile by ID
/// </summary>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(EmailProfileDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
{
var query = new GetEmailProfileByIdQuery(id);
var result = await mediator.Send(query, cancellationToken);
if (result == null)
return NotFound(new { Message = $"Email profile with ID {id} not found" });
return Ok(result);
}
/// <summary>
/// Get all active email profiles
/// </summary>
[HttpGet("active")]
[ProducesResponseType(typeof(IEnumerable<EmailProfileDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetActive(CancellationToken cancellationToken)
{
var query = new GetActiveEmailProfilesQuery();
var result = await mediator.Send(query, cancellationToken);
return Ok(result);
}
/// <summary>
/// Create new email profile (async via RabbitMQ)
/// </summary>
[HttpPost]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create(
[FromBody] CreateEmailProfileCommand command,
CancellationToken cancellationToken)
{
// Publish command to RabbitMQ for async processing
await commandPublisher.PublishAsync(command, cancellationToken);
return Accepted(new
{
Message = "Email profile creation request queued for processing",
ProfileName = command.ProfileName
});
}
/// <summary>
/// Update email profile (async via RabbitMQ)
/// </summary>
[HttpPut("{id:int}")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Update(
int id,
[FromBody] UpdateEmailProfileCommand command,
CancellationToken cancellationToken)
{
// Ensure ID matches route
if (id != command.Id)
return BadRequest(new { Message = "Route ID does not match command ID" });
// Publish command to RabbitMQ for async processing
await commandPublisher.PublishAsync(command, cancellationToken);
return Accepted(new
{
Message = "Email profile update request queued for processing",
Id = command.Id
});
}
/// <summary>
/// Delete email profile (async via RabbitMQ)
/// </summary>
[HttpDelete("{id:int}")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
{
var command = new DeleteEmailProfileCommand(id);
// Publish command to RabbitMQ for async processing
await commandPublisher.PublishAsync(command, cancellationToken);
return Accepted(new
{
Message = "Email profile deletion request queued for processing",
Id = id
});
}
}

View File

@@ -1,36 +0,0 @@
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
using DigitalData.EmailProfiler.Domain.Entities;
using MediatR;
namespace DigitalData.EmailProfiler.Application.EmailAccounts.Commands;
/// <summary>
/// Command to create a new email account.
/// </summary>
public record CreateEmailAccountCommand : IRequest<int>
{
public string AccountName { get; init; } = string.Empty;
public string Username { get; init; } = string.Empty;
public string ImapServer { get; init; } = string.Empty;
public int ImapPort { get; init; } = 993;
public bool ImapUseSsl { get; init; } = true;
public string SmtpServer { get; init; } = string.Empty;
public int SmtpPort { get; init; } = 587;
public bool SmtpUseSsl { get; init; } = true;
public bool UseOAuth2 { get; init; }
public string? EncryptedPassword { get; init; } // Already encrypted by client
public string? TenantId { get; init; }
public string? ClientId { get; init; }
public string? EncryptedClientSecret { get; init; } // Already encrypted by client
public bool IsActive { get; init; } = true;
}
public class CreateEmailAccountCommandHandler(IRepository<EmailAccount> repository)
: IRequestHandler<CreateEmailAccountCommand, int>
{
public async Task<int> Handle(CreateEmailAccountCommand request, CancellationToken cancellationToken)
{
var account = await repository.CreateAsync(request, cancellationToken);
return account.Id;
}
}

View File

@@ -1,26 +0,0 @@
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.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

@@ -1,26 +0,0 @@
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.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

@@ -1,56 +0,0 @@
using DigitalData.EmailProfiler.Application.EmailAccounts.Commands;
using FluentValidation;
namespace DigitalData.EmailProfiler.Application.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

@@ -1,25 +0,0 @@
using AutoMapper;
using DigitalData.EmailProfiler.Application.Common.Dtos.EmailHistories;
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
using MediatR;
namespace DigitalData.EmailProfiler.Application.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

@@ -1,48 +0,0 @@
using AutoMapper;
using DigitalData.EmailProfiler.Application.Common.Dtos.EmailHistories;
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
using MediatR;
namespace DigitalData.EmailProfiler.Application.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,180 +0,0 @@
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;
using DigitalData.EmailProfiler.Domain.Enums;
using DigitalData.EmailProfiler.Domain.Events;
using DigitalData.EmailProfiler.Domain.Exceptions;
using DigitalData.EmailProfiler.Domain.Services;
using MediatR;
namespace DigitalData.EmailProfiler.Application.EmailProcessing.Commands;
/// <summary>
/// Command to process a single email from a profile.
/// This is the core email processing logic.
/// </summary>
public record ProcessEmailCommand : IRequest<int>
{
public int ProfileId { get; init; }
public string MessageId { get; init; } = string.Empty;
public string Sender { get; init; } = string.Empty;
public DateTime ReceivedDate { get; init; }
public string Subject { get; init; } = string.Empty;
public string? BodyText { get; init; }
public string? BodyHtml { get; init; }
public List<AttachmentData> Attachments { get; init; } = new();
}
/// <summary>
/// Attachment data for email processing.
/// </summary>
public record AttachmentData(
string FileName,
byte[] Content,
string ContentType,
long SizeBytes);
public class ProcessEmailCommandHandler(
IEmailProfileRepository profileRepository,
IEmailHistoryRepository historyRepository,
IRepository<EmailAttachment> attachmentRepository,
IPublisher publisher,
MessageIdGenerator messageIdGenerator,
IPdfProcessingService pdfService,
IDmsService dmsService)
: IRequestHandler<ProcessEmailCommand, int>
{
public async Task<int> Handle(ProcessEmailCommand request, CancellationToken cancellationToken)
{
// 1. Get profile with related entities
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
var messageId = messageIdGenerator.Generate(
request.MessageId,
request.Sender,
request.ReceivedDate,
request.Subject);
// 3. Check for duplicates
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 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 historyRepository.CreateAsync(historyDto, cancellationToken);
try
{
// 5. Process attachments
foreach (var attachmentData in request.Attachments)
{
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);
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);
// For domain methods like MarkAsValid/MarkAsCorrupt, we need to get the entity
var attachmentEntity = await attachmentRepository.GetByIdAsync(attachment.Id, cancellationToken);
if (attachmentEntity != null)
{
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);
}
}
}
// 6. Archive to DMS if configured
if (profile.EmailProcess != null && profile.EmailProcess.EnableWindreamImport)
{
// TODO: Implement DMS archiving with indexing steps
// This will be implemented based on ProcessSteps and IndexingSteps
}
// 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(
new EmailProcessedEvent(
createdHistory.Id,
profile.Id,
messageId.Hash,
EmailStatus.Processed),
cancellationToken);
return createdHistory.Id;
}
catch (Exception ex)
{
// 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

@@ -1,50 +0,0 @@
using DigitalData.EmailProfiler.Application.EmailProcessing.Commands;
using FluentValidation;
namespace DigitalData.EmailProfiler.Application.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

@@ -1,28 +0,0 @@
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
using DigitalData.EmailProfiler.Domain.Entities;
using MediatR;
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
/// <summary>
/// Command to create a new email profile.
/// </summary>
public record CreateEmailProfileCommand : IRequest<int>
{
public string ProfileName { get; init; } = string.Empty;
public int EmailAccountId { get; init; }
public int? ProcessId { get; init; }
public string? ValidationSql { get; init; }
public int PollIntervalMinutes { get; init; } = 15;
public bool IsActive { get; init; } = true;
}
public class CreateEmailProfileCommandHandler(IRepository<EmailProfile> repository)
: IRequestHandler<CreateEmailProfileCommand, int>
{
public async Task<int> Handle(CreateEmailProfileCommand request, CancellationToken cancellationToken)
{
var profile = await repository.CreateAsync(request, cancellationToken);
return profile.Id;
}
}

View File

@@ -1,20 +0,0 @@
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
using DigitalData.EmailProfiler.Domain.Entities;
using MediatR;
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
/// <summary>
/// Command to delete an email profile.
/// </summary>
public record DeleteEmailProfileCommand(int Id) : IRequest<int>;
public class DeleteEmailProfileCommandHandler(IRepository<EmailProfile> repository)
: IRequestHandler<DeleteEmailProfileCommand, int>
{
public async Task<int> Handle(DeleteEmailProfileCommand request, CancellationToken cancellationToken)
{
await repository.DeleteSingleAsync(p => p.Id == request.Id, cancellationToken);
return request.Id;
}
}

View File

@@ -1,27 +0,0 @@
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
using DigitalData.EmailProfiler.Domain.Entities;
using MediatR;
namespace DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
/// <summary>
/// Command to update an existing email profile.
/// </summary>
public record UpdateEmailProfileCommand : IRequest<int>
{
public int Id { get; init; }
public string ProfileName { get; init; } = string.Empty;
public string? ValidationSql { get; init; }
public int PollIntervalMinutes { get; init; }
public bool IsActive { get; init; }
}
public class UpdateEmailProfileCommandHandler(IRepository<EmailProfile> repository)
: IRequestHandler<UpdateEmailProfileCommand, int>
{
public async Task<int> Handle(UpdateEmailProfileCommand request, CancellationToken cancellationToken)
{
await repository.UpdateSingleAsync(p => p.Id == request.Id, request, cancellationToken);
return request.Id;
}
}

View File

@@ -1,25 +0,0 @@
using AutoMapper;
using DigitalData.EmailProfiler.Application.Common.Dtos;
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
using MediatR;
namespace DigitalData.EmailProfiler.Application.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

@@ -1,25 +0,0 @@
using AutoMapper;
using DigitalData.EmailProfiler.Application.Common.Dtos;
using DigitalData.EmailProfiler.Application.Interfaces.Repositories;
using MediatR;
namespace DigitalData.EmailProfiler.Application.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

@@ -1,26 +0,0 @@
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.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

@@ -1,28 +0,0 @@
using DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
using FluentValidation;
namespace DigitalData.EmailProfiler.Application.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

@@ -1,28 +0,0 @@
using DigitalData.EmailProfiler.Application.EmailProfiles.Commands;
using FluentValidation;
namespace DigitalData.EmailProfiler.Application.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));
}
}