diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Interceptors/AuditInterceptor.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Interceptors/AuditInterceptor.cs
index 7480545..26e8a20 100644
--- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Interceptors/AuditInterceptor.cs
+++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Interceptors/AuditInterceptor.cs
@@ -4,13 +4,15 @@ using ColaFlow.Modules.ProjectManagement.Domain.Entities;
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
+using System.Text.Json;
+using System.Text.Json.Serialization;
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence.Interceptors;
///
/// EF Core SaveChangesInterceptor that automatically creates audit logs for all entity changes
/// Tracks Create/Update/Delete operations with tenant and user context
-/// Phase 1: Basic operation tracking (Phase 2 will add field-level changes)
+/// Phase 2: Field-level change detection with JSON diff
///
public class AuditInterceptor : SaveChangesInterceptor
{
@@ -49,11 +51,10 @@ public class AuditInterceptor : SaveChangesInterceptor
private void AuditChanges(DbContext context)
{
- try
- {
- var tenantId = TenantId.From(_tenantContext.GetCurrentTenantId());
- var userId = _tenantContext.GetCurrentUserId();
- UserId? userIdVO = userId.HasValue ? UserId.From(userId.Value) : null;
+ // Remove try-catch temporarily to see actual errors
+ var tenantId = TenantId.From(_tenantContext.GetCurrentTenantId());
+ var userId = _tenantContext.GetCurrentUserId();
+ UserId? userIdVO = userId.HasValue ? UserId.From(userId.Value) : null;
var entries = context.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added ||
@@ -79,31 +80,100 @@ public class AuditInterceptor : SaveChangesInterceptor
_ => "Unknown"
};
- // Phase 1: Basic operation tracking (no field-level changes)
- // Phase 2 will add OldValues/NewValues serialization
+ string? oldValues = null;
+ string? newValues = null;
+
+ // Phase 2: Field-level change detection
+ try
+ {
+ if (entry.State == EntityState.Modified)
+ {
+ // Get only changed scalar properties (excludes primary keys and navigation properties)
+ var changedProperties = entry.Properties
+ .Where(p => p.IsModified &&
+ !p.Metadata.IsPrimaryKey() &&
+ !p.Metadata.IsForeignKey() &&
+ p.Metadata.PropertyInfo != null) // Exclude shadow properties
+ .ToList();
+
+ if (changedProperties.Any())
+ {
+ var oldDict = new Dictionary();
+ var newDict = new Dictionary();
+
+ foreach (var prop in changedProperties)
+ {
+ oldDict[prop.Metadata.Name] = SerializableValue(prop.OriginalValue);
+ newDict[prop.Metadata.Name] = SerializableValue(prop.CurrentValue);
+ }
+
+ oldValues = JsonSerializer.Serialize(oldDict, new JsonSerializerOptions
+ {
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ });
+ newValues = JsonSerializer.Serialize(newDict, new JsonSerializerOptions
+ {
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ });
+ }
+ }
+ else if (entry.State == EntityState.Added)
+ {
+ // For Create, capture all current scalar values (except PK, FK, and shadow properties)
+ var currentDict = new Dictionary();
+ foreach (var prop in entry.Properties.Where(p =>
+ !p.Metadata.IsPrimaryKey() &&
+ !p.Metadata.IsForeignKey() &&
+ p.Metadata.PropertyInfo != null))
+ {
+ currentDict[prop.Metadata.Name] = SerializableValue(prop.CurrentValue);
+ }
+
+ newValues = JsonSerializer.Serialize(currentDict, new JsonSerializerOptions
+ {
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ });
+ }
+ else if (entry.State == EntityState.Deleted)
+ {
+ // For Delete, capture all original scalar values (except PK, FK, and shadow properties)
+ var originalDict = new Dictionary();
+ foreach (var prop in entry.Properties.Where(p =>
+ !p.Metadata.IsPrimaryKey() &&
+ !p.Metadata.IsForeignKey() &&
+ p.Metadata.PropertyInfo != null))
+ {
+ originalDict[prop.Metadata.Name] = SerializableValue(prop.OriginalValue);
+ }
+
+ oldValues = JsonSerializer.Serialize(originalDict, new JsonSerializerOptions
+ {
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ });
+ }
+ }
+ catch (Exception)
+ {
+ // If JSON serialization fails, continue without field-level changes
+ // but still create the audit log entry
+ }
+
var auditLog = AuditLog.Create(
tenantId: tenantId,
entityType: entityType,
entityId: entityId,
action: action,
userId: userIdVO,
- oldValues: null, // Phase 2: Will serialize old values
- newValues: null // Phase 2: Will serialize new values
+ oldValues: oldValues,
+ newValues: newValues
);
context.Add(auditLog);
}
- }
- catch (InvalidOperationException)
- {
- // Tenant context not available (e.g., during migrations, seeding)
- // Skip audit logging for system operations
- }
- catch (UnauthorizedAccessException)
- {
- // Tenant ID not found in claims (e.g., during background jobs)
- // Skip audit logging for unauthorized contexts
- }
}
///
@@ -128,4 +198,24 @@ public class AuditInterceptor : SaveChangesInterceptor
}
return Guid.Empty;
}
+
+ ///
+ /// Converts a value to a JSON-serializable format
+ /// Handles value objects (TenantId, UserId) by extracting their underlying values
+ ///
+ private object? SerializableValue(object? value)
+ {
+ if (value == null)
+ return null;
+
+ // Handle value objects by extracting their Value property
+ if (value is TenantId tenantId)
+ return tenantId.Value;
+
+ if (value is UserId userId)
+ return userId.Value;
+
+ // For other types, return as-is (primitives, strings, etc.)
+ return value;
+ }
}
diff --git a/docs/plans/sprint_2_story_2_task_1.md b/docs/plans/sprint_2_story_2_task_1.md
index be9f839..2c062f3 100644
--- a/docs/plans/sprint_2_story_2_task_1.md
+++ b/docs/plans/sprint_2_story_2_task_1.md
@@ -1,9 +1,10 @@
---
task_id: sprint_2_story_2_task_1
story: sprint_2_story_2
-status: not_started
+status: in_progress
estimated_hours: 6
created_date: 2025-11-05
+start_date: 2025-11-05
assignee: Backend Team
---