--- task_id: sprint_2_story_1_task_3 story: sprint_2_story_1 status: completed estimated_hours: 6 actual_hours: 6 created_date: 2025-11-05 start_date: 2025-11-05 completion_date: 2025-11-05 assignee: Backend Team --- # Task 3: Implement EF Core SaveChangesInterceptor **Story**: Story 1 - Audit Log Foundation (Phase 1) **Estimated**: 6 hours ## Description Implement EF Core SaveChangesInterceptor to automatically capture and log all Create/Update/Delete operations on auditable entities. This is the core mechanism for transparent audit logging. ## Acceptance Criteria - [ ] AuditLogInterceptor class created - [ ] Automatic detection of Create/Update/Delete operations - [ ] Audit log entries created for each operation - [ ] TenantId and UserId automatically captured - [ ] Interceptor registered in DI container - [ ] Performance overhead < 5ms per SaveChanges ## Implementation Details **Files to Create**: 1. **Interceptor**: `colaflow-api/src/ColaFlow.Infrastructure/Interceptors/AuditLogInterceptor.cs` ```csharp public class AuditLogInterceptor : SaveChangesInterceptor { private readonly ITenantContext _tenantContext; private readonly IHttpContextAccessor _httpContextAccessor; public AuditLogInterceptor(ITenantContext tenantContext, IHttpContextAccessor httpContextAccessor) { _tenantContext = tenantContext; _httpContextAccessor = httpContextAccessor; } public override async ValueTask> SavingChangesAsync( DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { if (eventData.Context is null) return result; var auditEntries = CreateAuditEntries(eventData.Context); // Add audit logs to context foreach (var auditEntry in auditEntries) { eventData.Context.Add(auditEntry); } return result; } private List CreateAuditEntries(DbContext context) { var auditLogs = new List(); var entries = context.ChangeTracker.Entries() .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified || e.State == EntityState.Deleted) .Where(e => IsAuditable(e.Entity)) .ToList(); foreach (var entry in entries) { var auditLog = new AuditLog { Id = Guid.NewGuid(), TenantId = _tenantContext.TenantId, EntityType = entry.Entity.GetType().Name, EntityId = GetEntityId(entry), Action = entry.State switch { EntityState.Added => AuditAction.Create, EntityState.Modified => AuditAction.Update, EntityState.Deleted => AuditAction.Delete, _ => throw new InvalidOperationException() }, UserId = GetCurrentUserId(), Timestamp = DateTime.UtcNow, OldValues = entry.State == EntityState.Modified ? SerializeOldValues(entry) : null, NewValues = entry.State != EntityState.Deleted ? SerializeNewValues(entry) : null }; auditLogs.Add(auditLog); } return auditLogs; } private bool IsAuditable(object entity) { // Check if entity implements IAuditable interface or is in auditable types list return entity is Project || entity is Epic || entity is Story || entity is WorkTask; } private Guid GetEntityId(EntityEntry entry) { var keyValue = entry.Properties.FirstOrDefault(p => p.Metadata.IsPrimaryKey())?.CurrentValue; return keyValue is Guid id ? id : Guid.Empty; } private Guid? GetCurrentUserId() { var userIdClaim = _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier); return userIdClaim != null ? Guid.Parse(userIdClaim.Value) : null; } private string? SerializeOldValues(EntityEntry entry) { var oldValues = entry.Properties .Where(p => p.IsModified) .ToDictionary(p => p.Metadata.Name, p => p.OriginalValue); return JsonSerializer.Serialize(oldValues); } private string? SerializeNewValues(EntityEntry entry) { var newValues = entry.Properties .Where(p => !p.Metadata.IsPrimaryKey()) .ToDictionary(p => p.Metadata.Name, p => p.CurrentValue); return JsonSerializer.Serialize(newValues); } } ``` 2. **DI Registration**: Update `colaflow-api/src/ColaFlow.Infrastructure/DependencyInjection.cs` ```csharp services.AddDbContext((serviceProvider, options) => { options.UseNpgsql(connectionString); options.AddInterceptors(serviceProvider.GetRequiredService()); }); services.AddScoped(); ``` ## Technical Notes - Phase 1 captures basic Create/Update/Delete operations - Changed Fields tracking (old vs new values diff) will be enhanced in Phase 2 - Performance optimization: Consider async operations and batching - Use System.Text.Json for serialization (faster than Newtonsoft.Json) ## Testing - Unit tests for interceptor logic - Integration tests for automatic audit logging - Performance benchmark: < 5ms overhead - Verify TenantId and UserId are captured correctly --- **Created**: 2025-11-05 by Backend Agent