feat(api): Add REST API controllers with RabbitMQ for POST/PUT/DELETE

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<T>)
- 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
This commit is contained in:
2026-07-14 16:39:35 +02:00
parent 0d22fe0b5c
commit bfe24eba06
5 changed files with 253 additions and 3 deletions

View File

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

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

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

@@ -10,10 +10,10 @@ public interface ICommandPublisher
/// <summary>
/// Publishes a command to the message broker for asynchronous processing
/// </summary>
/// <typeparam name="TCommand">The command type (must implement IRequest)</typeparam>
/// <typeparam name="TCommand">The command type (must implement IBaseRequest - covers both IRequest and IRequest<T>)</typeparam>
/// <param name="command">The command to publish</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Task representing the publish operation</returns>
Task PublishAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
where TCommand : IRequest;
where TCommand : IBaseRequest;
}

View File

@@ -84,7 +84,7 @@ public class RabbitMqCommandPublisher : ICommandPublisher, IDisposable
/// Publishes a command to RabbitMQ for asynchronous processing
/// </summary>
public async Task PublishAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
where TCommand : IRequest
where TCommand : IBaseRequest
{
ObjectDisposedException.ThrowIf(Disposed, typeof(RabbitMqCommandPublisher));