From bfe24eba06371233a019346135ac0a36f9c269d1 Mon Sep 17 00:00:00 2001 From: TekH Date: Tue, 14 Jul 2026 16:39:35 +0200 Subject: [PATCH] feat(api): Add REST API controllers with RabbitMQ for POST/PUT/DELETE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Controllers: - EmailProfilesController: CRUD operations (GET sync, POST/PUT/DELETE async via RabbitMQ) * GET /api/emailprofiles - List all profiles * GET /api/emailprofiles/{id} - Get profile by ID * GET /api/emailprofiles/active - List active profiles * POST /api/emailprofiles - Create (202 Accepted, queued to RabbitMQ) * PUT /api/emailprofiles/{id} - Update (202 Accepted, queued to RabbitMQ) * DELETE /api/emailprofiles/{id} - Delete (202 Accepted, queued to RabbitMQ) - EmailAccountsController: CRUD operations * GET /api/emailaccounts - List all accounts * GET /api/emailaccounts/{id} - Get account by ID * POST /api/emailaccounts - Create (202 Accepted, queued to RabbitMQ) - EmailHistoryController: Read-only operations * GET /api/emailhistory/profile/{profileId} - Get history with pagination * GET /api/emailhistory/{id} - Get history by ID Changes: - Fix ICommandPublisher constraint: IRequest → IBaseRequest (supports IRequest) - All POST/PUT/DELETE return HTTP 202 Accepted (async processing) - All GET operations return HTTP 200 OK (synchronous via MediatR) - Proper error handling: 404 Not Found for missing resources --- .../Controllers/EmailAccountsController.cs | 70 ++++++++++ .../Controllers/EmailHistoryController.cs | 57 ++++++++ .../Controllers/EmailProfilesController.cs | 123 ++++++++++++++++++ .../Common/Interfaces/ICommandPublisher.cs | 4 +- .../Messaging/RabbitMqCommandPublisher.cs | 2 +- 5 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 src/DigitalData.EmailProfiler.API/Controllers/EmailAccountsController.cs create mode 100644 src/DigitalData.EmailProfiler.API/Controllers/EmailHistoryController.cs create mode 100644 src/DigitalData.EmailProfiler.API/Controllers/EmailProfilesController.cs diff --git a/src/DigitalData.EmailProfiler.API/Controllers/EmailAccountsController.cs b/src/DigitalData.EmailProfiler.API/Controllers/EmailAccountsController.cs new file mode 100644 index 0000000..d448dc6 --- /dev/null +++ b/src/DigitalData.EmailProfiler.API/Controllers/EmailAccountsController.cs @@ -0,0 +1,70 @@ +using DigitalData.EmailProfiler.Application.Common.Dtos; +using DigitalData.EmailProfiler.Application.Common.Interfaces; +using DigitalData.EmailProfiler.Application.Features.EmailAccounts.Commands; +using DigitalData.EmailProfiler.Application.Features.EmailAccounts.Queries; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace DigitalData.EmailProfiler.API.Controllers; + +/// +/// Email accounts management API controller +/// +[ApiController] +[Route("api/[controller]")] +public class EmailAccountsController( + IMediator mediator, + ICommandPublisher commandPublisher) : ControllerBase +{ + /// + /// Get all email accounts + /// + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task GetAll(CancellationToken cancellationToken) + { + var query = new GetEmailAccountsQuery(); + var result = await mediator.Send(query, cancellationToken); + return Ok(result); + } + + /// + /// Get email account by ID + /// + [HttpGet("{id:int}")] + [ProducesResponseType(typeof(EmailAccountDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task 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); + } + + /// + /// Create new email account (async via RabbitMQ) + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status202Accepted)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task 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 +} diff --git a/src/DigitalData.EmailProfiler.API/Controllers/EmailHistoryController.cs b/src/DigitalData.EmailProfiler.API/Controllers/EmailHistoryController.cs new file mode 100644 index 0000000..c377e24 --- /dev/null +++ b/src/DigitalData.EmailProfiler.API/Controllers/EmailHistoryController.cs @@ -0,0 +1,57 @@ +using DigitalData.EmailProfiler.Application.Common.Dtos.EmailHistories; +using DigitalData.EmailProfiler.Application.Features.EmailHistories.Queries; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace DigitalData.EmailProfiler.API.Controllers; + +/// +/// Email history API controller (read-only) +/// +[ApiController] +[Route("api/[controller]")] +public class EmailHistoryController(IMediator mediator) : ControllerBase +{ + /// + /// Get email history by profile ID with pagination + /// + [HttpGet("profile/{profileId:int}")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task 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 + }); + } + + /// + /// Get email history by ID + /// + [HttpGet("{id:int}")] + [ProducesResponseType(typeof(EmailHistoryDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task 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 +} diff --git a/src/DigitalData.EmailProfiler.API/Controllers/EmailProfilesController.cs b/src/DigitalData.EmailProfiler.API/Controllers/EmailProfilesController.cs new file mode 100644 index 0000000..68772e0 --- /dev/null +++ b/src/DigitalData.EmailProfiler.API/Controllers/EmailProfilesController.cs @@ -0,0 +1,123 @@ +using DigitalData.EmailProfiler.Application.Common.Dtos; +using DigitalData.EmailProfiler.Application.Common.Interfaces; +using DigitalData.EmailProfiler.Application.Features.EmailProfiles.Commands; +using DigitalData.EmailProfiler.Application.Features.EmailProfiles.Queries; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace DigitalData.EmailProfiler.API.Controllers; + +/// +/// Email profiles management API controller +/// +[ApiController] +[Route("api/[controller]")] +public class EmailProfilesController( + IMediator mediator, + ICommandPublisher commandPublisher) : ControllerBase +{ + /// + /// Get all email profiles + /// + [HttpGet] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task GetAll(CancellationToken cancellationToken) + { + var query = new GetEmailProfilesQuery(); + var result = await mediator.Send(query, cancellationToken); + return Ok(result); + } + + /// + /// Get email profile by ID + /// + [HttpGet("{id:int}")] + [ProducesResponseType(typeof(EmailProfileDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task 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); + } + + /// + /// Get all active email profiles + /// + [HttpGet("active")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task GetActive(CancellationToken cancellationToken) + { + var query = new GetActiveEmailProfilesQuery(); + var result = await mediator.Send(query, cancellationToken); + return Ok(result); + } + + /// + /// Create new email profile (async via RabbitMQ) + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status202Accepted)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task 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 + }); + } + + /// + /// Update email profile (async via RabbitMQ) + /// + [HttpPut("{id:int}")] + [ProducesResponseType(StatusCodes.Status202Accepted)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task 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 + }); + } + + /// + /// Delete email profile (async via RabbitMQ) + /// + [HttpDelete("{id:int}")] + [ProducesResponseType(StatusCodes.Status202Accepted)] + public async Task 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 + }); + } +} diff --git a/src/DigitalData.EmailProfiler.Application/Common/Interfaces/ICommandPublisher.cs b/src/DigitalData.EmailProfiler.Application/Common/Interfaces/ICommandPublisher.cs index e63c4e3..5fb22c7 100644 --- a/src/DigitalData.EmailProfiler.Application/Common/Interfaces/ICommandPublisher.cs +++ b/src/DigitalData.EmailProfiler.Application/Common/Interfaces/ICommandPublisher.cs @@ -10,10 +10,10 @@ public interface ICommandPublisher /// /// Publishes a command to the message broker for asynchronous processing /// - /// The command type (must implement IRequest) + /// The command type (must implement IBaseRequest - covers both IRequest and IRequest) /// The command to publish /// Cancellation token /// Task representing the publish operation Task PublishAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : IRequest; + where TCommand : IBaseRequest; } diff --git a/src/DigitalData.EmailProfiler.Infrastructure/Messaging/RabbitMqCommandPublisher.cs b/src/DigitalData.EmailProfiler.Infrastructure/Messaging/RabbitMqCommandPublisher.cs index ea179d7..11e67cc 100644 --- a/src/DigitalData.EmailProfiler.Infrastructure/Messaging/RabbitMqCommandPublisher.cs +++ b/src/DigitalData.EmailProfiler.Infrastructure/Messaging/RabbitMqCommandPublisher.cs @@ -84,7 +84,7 @@ public class RabbitMqCommandPublisher : ICommandPublisher, IDisposable /// Publishes a command to RabbitMQ for asynchronous processing /// public async Task PublishAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : IRequest + where TCommand : IBaseRequest { ObjectDisposedException.ThrowIf(Disposed, typeof(RabbitMqCommandPublisher));