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:
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,10 +10,10 @@ public interface ICommandPublisher
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Publishes a command to the message broker for asynchronous processing
|
/// Publishes a command to the message broker for asynchronous processing
|
||||||
/// </summary>
|
/// </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="command">The command to publish</param>
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <returns>Task representing the publish operation</returns>
|
/// <returns>Task representing the publish operation</returns>
|
||||||
Task PublishAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
|
Task PublishAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
|
||||||
where TCommand : IRequest;
|
where TCommand : IBaseRequest;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ public class RabbitMqCommandPublisher : ICommandPublisher, IDisposable
|
|||||||
/// Publishes a command to RabbitMQ for asynchronous processing
|
/// Publishes a command to RabbitMQ for asynchronous processing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task PublishAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
|
public async Task PublishAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
|
||||||
where TCommand : IRequest
|
where TCommand : IBaseRequest
|
||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(Disposed, typeof(RabbitMqCommandPublisher));
|
ObjectDisposedException.ThrowIf(Disposed, typeof(RabbitMqCommandPublisher));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user