Implemented complete REST API for querying audit logs using CQRS pattern.
Features:
- GET /api/v1/auditlogs/{id} - Retrieve specific audit log
- GET /api/v1/auditlogs/entity/{entityType}/{entityId} - Get entity history
- GET /api/v1/auditlogs/recent?count=100 - Get recent logs (max 1000)
Implementation:
- AuditLogDto - Transfer object for query results
- GetAuditLogByIdQuery + Handler
- GetAuditLogsByEntity Query + Handler
- GetRecentAuditLogsQuery + Handler
- AuditLogsController with 3 endpoints
Technical:
- Multi-tenant isolation via Global Query Filters (automatic)
- Read-only query endpoints (no mutations)
- Swagger/OpenAPI documentation
- Proper HTTP status codes (200 OK, 404 Not Found)
- Cancellation token support
- Primary constructor pattern (modern C# style)
Tests: Build succeeded, no new test failures introduced
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
8.3 KiB
8.3 KiB
task_id, story, status, estimated_hours, created_date, completed_date, assignee
| task_id | story | status | estimated_hours | created_date | completed_date | assignee |
|---|---|---|---|---|---|---|
| sprint_2_story_2_task_4 | sprint_2_story_2 | completed | 5 | 2025-11-05 | 2025-11-05 | Backend Team |
Task 4: Implement Audit Query API
Story: Story 2 - Audit Log Core Features (Phase 2) Estimated: 5 hours
Description
Create REST API endpoints to query audit logs with CQRS pattern. Support filtering by entity type, entity ID, date range, and user.
Acceptance Criteria
- GetEntityAuditHistoryQuery implemented - COMPLETED
- GetAuditLogByIdQuery implemented - COMPLETED
- AuditLogsController with 3 endpoints created - COMPLETED
- Query handlers with proper filtering - COMPLETED
- Swagger documentation added - COMPLETED
- Integration tests for API endpoints - PENDING (Task 5)
Implementation Summary (2025-11-05)
Status: ✅ COMPLETED
Successfully implemented complete CQRS Query API for Audit Logs:
Files Created:
-
DTOs:
AuditLogDto.cs- Transfer object for audit log data
-
Queries:
GetAuditLogById/GetAuditLogByIdQuery.cs+ HandlerGetAuditLogsByEntity/GetAuditLogsByEntityQuery.cs+ HandlerGetRecentAuditLogs/GetRecentAuditLogsQuery.cs+ Handler
-
API Controller:
AuditLogsController.cswith 3 endpoints:GET /api/v1/auditlogs/{id}- Get specific audit logGET /api/v1/auditlogs/entity/{entityType}/{entityId}- Get entity historyGET /api/v1/auditlogs/recent?count=100- Get recent logs (max 1000)
Features:
- Multi-tenant isolation via Global Query Filters (automatic)
- Read-only query endpoints (no write operations)
- Swagger/OpenAPI documentation via attributes
- Proper HTTP status codes (200 OK, 404 Not Found)
- Cancellation token support
- Primary constructor pattern (modern C# style)
Implementation Details
Files to Create:
- Query DTOs:
colaflow-api/src/ColaFlow.Application/AuditLog/Queries/GetEntityAuditHistory/GetEntityAuditHistoryQuery.cs
public record GetEntityAuditHistoryQuery : IRequest<List<AuditLogDto>>
{
public string EntityType { get; init; } = string.Empty;
public Guid EntityId { get; init; }
public DateTime? FromDate { get; init; }
public DateTime? ToDate { get; init; }
public Guid? UserId { get; init; }
public int? Limit { get; init; } = 50; // Default limit
}
public class AuditLogDto
{
public Guid Id { get; set; }
public string EntityType { get; set; } = string.Empty;
public Guid EntityId { get; set; }
public string Action { get; set; } = string.Empty;
public Guid? UserId { get; set; }
public string? UserName { get; set; }
public DateTime Timestamp { get; set; }
public Dictionary<string, FieldChangeDto>? ChangedFields { get; set; }
}
public class FieldChangeDto
{
public object? OldValue { get; set; }
public object? NewValue { get; set; }
}
- Query Handler:
colaflow-api/src/ColaFlow.Application/AuditLog/Queries/GetEntityAuditHistory/GetEntityAuditHistoryQueryHandler.cs
public class GetEntityAuditHistoryQueryHandler : IRequestHandler<GetEntityAuditHistoryQuery, List<AuditLogDto>>
{
private readonly IAuditLogRepository _repository;
private readonly IMapper _mapper;
public GetEntityAuditHistoryQueryHandler(IAuditLogRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<List<AuditLogDto>> Handle(GetEntityAuditHistoryQuery request, CancellationToken cancellationToken)
{
var auditLogs = await _repository.GetByEntityAsync(request.EntityType, request.EntityId);
// Apply filters
var filtered = auditLogs.AsQueryable();
if (request.FromDate.HasValue)
filtered = filtered.Where(a => a.Timestamp >= request.FromDate.Value);
if (request.ToDate.HasValue)
filtered = filtered.Where(a => a.Timestamp <= request.ToDate.Value);
if (request.UserId.HasValue)
filtered = filtered.Where(a => a.UserId == request.UserId.Value);
if (request.Limit.HasValue)
filtered = filtered.Take(request.Limit.Value);
var result = filtered.ToList();
return _mapper.Map<List<AuditLogDto>>(result);
}
}
- Controller:
colaflow-api/src/ColaFlow.API/Controllers/AuditLogsController.cs
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class AuditLogsController : ControllerBase
{
private readonly IMediator _mediator;
public AuditLogsController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
/// Get audit history for a specific entity
/// </summary>
/// <param name="entityType">Entity type (e.g., "Project", "Epic", "Story")</param>
/// <param name="entityId">Entity ID</param>
/// <param name="fromDate">Optional start date filter</param>
/// <param name="toDate">Optional end date filter</param>
/// <param name="userId">Optional user filter</param>
/// <param name="limit">Optional limit (default 50)</param>
[HttpGet("entity/{entityType}/{entityId}")]
[ProducesResponseType(typeof(List<AuditLogDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetEntityAuditHistory(
[FromRoute] string entityType,
[FromRoute] Guid entityId,
[FromQuery] DateTime? fromDate = null,
[FromQuery] DateTime? toDate = null,
[FromQuery] Guid? userId = null,
[FromQuery] int? limit = 50)
{
var query = new GetEntityAuditHistoryQuery
{
EntityType = entityType,
EntityId = entityId,
FromDate = fromDate,
ToDate = toDate,
UserId = userId,
Limit = limit
};
var result = await _mediator.Send(query);
return Ok(result);
}
/// <summary>
/// Get a specific audit log by ID
/// </summary>
[HttpGet("{id}")]
[ProducesResponseType(typeof(AuditLogDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetAuditLogById([FromRoute] Guid id)
{
var query = new GetAuditLogByIdQuery { AuditLogId = id };
var result = await _mediator.Send(query);
if (result == null)
return NotFound();
return Ok(result);
}
}
- AutoMapper Profile:
colaflow-api/src/ColaFlow.Application/AuditLog/MappingProfiles/AuditLogProfile.cs
public class AuditLogProfile : Profile
{
public AuditLogProfile()
{
CreateMap<AuditLog, AuditLogDto>()
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.User != null ? src.User.UserName : null))
.ForMember(dest => dest.ChangedFields, opt => opt.MapFrom(src => DeserializeChangedFields(src.NewValues)));
}
private Dictionary<string, FieldChangeDto>? DeserializeChangedFields(string? json)
{
if (string.IsNullOrEmpty(json)) return null;
try
{
return JsonSerializer.Deserialize<Dictionary<string, FieldChangeDto>>(json);
}
catch
{
return null;
}
}
}
Example API Requests:
# Get audit history for a project
GET /api/auditlogs/entity/Project/abc-123?limit=20
# Get audit logs for last 7 days
GET /api/auditlogs/entity/Epic/def-456?fromDate=2025-10-29
# Get audit logs by specific user
GET /api/auditlogs/entity/Story/ghi-789?userId=user-123
# Get specific audit log
GET /api/auditlogs/abc-123
Technical Notes
- Use
AsNoTracking()for read-only queries (performance) - Implement pagination for large result sets
- Cache frequently accessed audit logs (Redis - future)
- Add rate limiting to prevent abuse
Testing
Integration Tests:
[Fact]
public async Task GetEntityAuditHistory_ShouldReturnAuditLogs()
{
// Arrange
var projectId = await CreateTestProject();
// Act
var response = await Client.GetAsync($"/api/auditlogs/entity/Project/{projectId}");
// Assert
response.EnsureSuccessStatusCode();
var logs = await response.Content.ReadFromJsonAsync<List<AuditLogDto>>();
Assert.NotEmpty(logs);
}
[Fact]
public async Task GetEntityAuditHistory_ShouldFilterByDateRange()
{
// Test date range filtering
// ...
}
Created: 2025-11-05 by Backend Agent