Loading src/api/Program.cs +0 −1 Original line number Diff line number Diff line Loading @@ -98,7 +98,6 @@ builder.Services.AddSwaggerGen(options => // integrate xml comments options.IncludeXmlComments(XmlCommentsFilePath()); }); //// ############# AUTHORIZATION ################ Loading src/infrastructure/Contexts/AuditSaveChangesInterceptor.cs +61 −69 Original line number Diff line number Diff line Loading @@ -3,7 +3,9 @@ using Cals.Visor.Application; using Cals.Visor.Application.Identity; using Cals.Visor.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; namespace Cals.Visor.Infrastructure.Contexts; Loading @@ -12,102 +14,92 @@ public class AuditSaveChangesInterceptor : SaveChangesInterceptor private readonly ICurrentUser _currentUser; private readonly ICorrelationIdAccessor _correlationIdAccessor; // Store audit logs created before SaveChanges private readonly List<(EntityEntry Entry, Dictionary<string, object?> Changes, EntityState OriginalState)> _pendingAuditLogs = new(); public AuditSaveChangesInterceptor(ICurrentUser currentUser, ICorrelationIdAccessor correlationIdAccessor) { _currentUser = currentUser; _correlationIdAccessor = correlationIdAccessor; } private long? GetUserId() => _currentUser.CurrentUser?.Id; private string? GetCorrelationId() => _correlationIdAccessor.CorrelationId; public override ValueTask<InterceptionResult<int>> SavingChangesAsync( DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default) { var context = eventData.Context; DbContext? context = eventData.Context; if (context == null) return base.SavingChangesAsync(eventData, result, cancellationToken); var auditLogs = BuildAuditLogs(context); _pendingAuditLogs.Clear(); if (auditLogs.Count > 0) context.Set<AuditLog>().AddRange(auditLogs); foreach (EntityEntry? entry in context.ChangeTracker.Entries() .Where(e => e.Entity is IAuditableEntity && e.Entity is not AuditLog && e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)) { Dictionary<string, object?> changes = CollectChanges(entry); if (changes.Count > 0) _pendingAuditLogs.Add((entry,changes, entry.State)); } return base.SavingChangesAsync(eventData, result, cancellationToken); } public override async ValueTask<int> SavedChangesAsync( SaveChangesCompletedEventData eventData, int result, CancellationToken cancellationToken = default) { DbContext? context = eventData.Context; if (context == null || _pendingAuditLogs.Count == 0) return await base.SavedChangesAsync(eventData, result, cancellationToken); private List<AuditLog> BuildAuditLogs(DbContext context) foreach ((EntityEntry? entry, Dictionary<string, object?>? changes, EntityState originalState) in _pendingAuditLogs) { var entries = context.ChangeTracker.Entries() .Where(e => e.Entity is not AuditLog && e.Entity is IAuditableEntity && e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) .ToList(); IEntityType entityType = entry.Metadata; PropertyEntry keyProp = entry.Properties.First(p => p.Metadata.IsPrimaryKey()); string? keyValue = keyProp.CurrentValue?.ToString(); var userId = GetUserId(); var correlationId = GetCorrelationId(); var audit = new AuditLog { TableName = entityType.GetTableName()!, EntityId = keyValue ?? string.Empty, Action = originalState.ToString().ToUpperInvariant(), AppUserId = _currentUser.CurrentUser?.Id, CorrelationId = _correlationIdAccessor.CorrelationId, CreatedAt = DateTime.UtcNow, ChangesJson = JsonSerializer.Serialize(changes) }; var logs = new List<AuditLog>(); context.Add(audit); } foreach (var entry in entries) { var tableName = context.Model.FindEntityType(entry.Entity.GetType())?.GetTableName()!; var keyName = entry.Properties.First(p => p.Metadata.IsPrimaryKey()).Metadata.Name; var keyValue = entry.Property(keyName).CurrentValue?.ToString() ?? ""; // This ensures audit logs are saved with the real key value await context.SaveChangesAsync(cancellationToken); return await base.SavedChangesAsync(eventData, result, cancellationToken); } private static Dictionary<string, object?> CollectChanges(EntityEntry entry) { var changes = new Dictionary<string, object?>(); foreach (var prop in entry.Properties) foreach (PropertyEntry prop in entry.Properties) { if (prop.IsTemporary) continue; if (prop.Metadata.IsPrimaryKey()) continue; if (prop.IsTemporary || prop.Metadata.IsPrimaryKey()) continue; if (entry.State == EntityState.Added) { changes[prop.Metadata.Name] = new { Old = (object?)null, New = prop.CurrentValue }; } else if (entry.State == EntityState.Modified && prop.IsModified) { changes[prop.Metadata.Name] = new { Old = prop.OriginalValue, New = prop.CurrentValue }; } changes[prop.Metadata.Name] = new { Old = prop.OriginalValue, New = prop.CurrentValue }; else if (entry.State == EntityState.Deleted) { changes[prop.Metadata.Name] = new { Old = prop.OriginalValue, New = (object?)null }; } } if (changes.Count == 0) continue; logs.Add(new AuditLog { TableName = tableName, EntityId = keyValue, Action = entry.State.ToString().ToUpper(), AppUserId = userId, CorrelationId = correlationId, CreatedAt = DateTime.UtcNow, ChangesJson = JsonSerializer.Serialize(changes) }); //// Update AuditableEntity fields //if (entry.Entity is AuditableEntity auditable && // entry.State == EntityState.Modified) //{ // auditable.LastModifiedBy = userId; // auditable.LastModifiedUtc = DateTime.UtcNow; //} } return logs; return changes; } } src/models/Area.cs +1 −1 Original line number Diff line number Diff line namespace Cals.Visor.Models; public class Area : ICreatedAt, IUpdatedAt public class Area : ICreatedAt, IUpdatedAt, IAuditableEntity { public long? AreaId { get; set; } Loading src/models/InfobloxDomain.cs +1 −1 Original line number Diff line number Diff line namespace Cals.Visor.Models; public class InfobloxDomain : ICreatedAt, IUpdatedAt public class InfobloxDomain : ICreatedAt, IUpdatedAt, IAuditableEntity { public long? InfobloxDomainId { get; set; } Loading src/models/InfobloxGroup.cs +1 −1 Original line number Diff line number Diff line namespace Cals.Visor.Models; public class InfobloxGroup : ICreatedAt public class InfobloxGroup : ICreatedAt, IAuditableEntity { public long? InfobloxGroupId { get; set; } Loading Loading
src/api/Program.cs +0 −1 Original line number Diff line number Diff line Loading @@ -98,7 +98,6 @@ builder.Services.AddSwaggerGen(options => // integrate xml comments options.IncludeXmlComments(XmlCommentsFilePath()); }); //// ############# AUTHORIZATION ################ Loading
src/infrastructure/Contexts/AuditSaveChangesInterceptor.cs +61 −69 Original line number Diff line number Diff line Loading @@ -3,7 +3,9 @@ using Cals.Visor.Application; using Cals.Visor.Application.Identity; using Cals.Visor.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; namespace Cals.Visor.Infrastructure.Contexts; Loading @@ -12,102 +14,92 @@ public class AuditSaveChangesInterceptor : SaveChangesInterceptor private readonly ICurrentUser _currentUser; private readonly ICorrelationIdAccessor _correlationIdAccessor; // Store audit logs created before SaveChanges private readonly List<(EntityEntry Entry, Dictionary<string, object?> Changes, EntityState OriginalState)> _pendingAuditLogs = new(); public AuditSaveChangesInterceptor(ICurrentUser currentUser, ICorrelationIdAccessor correlationIdAccessor) { _currentUser = currentUser; _correlationIdAccessor = correlationIdAccessor; } private long? GetUserId() => _currentUser.CurrentUser?.Id; private string? GetCorrelationId() => _correlationIdAccessor.CorrelationId; public override ValueTask<InterceptionResult<int>> SavingChangesAsync( DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default) { var context = eventData.Context; DbContext? context = eventData.Context; if (context == null) return base.SavingChangesAsync(eventData, result, cancellationToken); var auditLogs = BuildAuditLogs(context); _pendingAuditLogs.Clear(); if (auditLogs.Count > 0) context.Set<AuditLog>().AddRange(auditLogs); foreach (EntityEntry? entry in context.ChangeTracker.Entries() .Where(e => e.Entity is IAuditableEntity && e.Entity is not AuditLog && e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)) { Dictionary<string, object?> changes = CollectChanges(entry); if (changes.Count > 0) _pendingAuditLogs.Add((entry,changes, entry.State)); } return base.SavingChangesAsync(eventData, result, cancellationToken); } public override async ValueTask<int> SavedChangesAsync( SaveChangesCompletedEventData eventData, int result, CancellationToken cancellationToken = default) { DbContext? context = eventData.Context; if (context == null || _pendingAuditLogs.Count == 0) return await base.SavedChangesAsync(eventData, result, cancellationToken); private List<AuditLog> BuildAuditLogs(DbContext context) foreach ((EntityEntry? entry, Dictionary<string, object?>? changes, EntityState originalState) in _pendingAuditLogs) { var entries = context.ChangeTracker.Entries() .Where(e => e.Entity is not AuditLog && e.Entity is IAuditableEntity && e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) .ToList(); IEntityType entityType = entry.Metadata; PropertyEntry keyProp = entry.Properties.First(p => p.Metadata.IsPrimaryKey()); string? keyValue = keyProp.CurrentValue?.ToString(); var userId = GetUserId(); var correlationId = GetCorrelationId(); var audit = new AuditLog { TableName = entityType.GetTableName()!, EntityId = keyValue ?? string.Empty, Action = originalState.ToString().ToUpperInvariant(), AppUserId = _currentUser.CurrentUser?.Id, CorrelationId = _correlationIdAccessor.CorrelationId, CreatedAt = DateTime.UtcNow, ChangesJson = JsonSerializer.Serialize(changes) }; var logs = new List<AuditLog>(); context.Add(audit); } foreach (var entry in entries) { var tableName = context.Model.FindEntityType(entry.Entity.GetType())?.GetTableName()!; var keyName = entry.Properties.First(p => p.Metadata.IsPrimaryKey()).Metadata.Name; var keyValue = entry.Property(keyName).CurrentValue?.ToString() ?? ""; // This ensures audit logs are saved with the real key value await context.SaveChangesAsync(cancellationToken); return await base.SavedChangesAsync(eventData, result, cancellationToken); } private static Dictionary<string, object?> CollectChanges(EntityEntry entry) { var changes = new Dictionary<string, object?>(); foreach (var prop in entry.Properties) foreach (PropertyEntry prop in entry.Properties) { if (prop.IsTemporary) continue; if (prop.Metadata.IsPrimaryKey()) continue; if (prop.IsTemporary || prop.Metadata.IsPrimaryKey()) continue; if (entry.State == EntityState.Added) { changes[prop.Metadata.Name] = new { Old = (object?)null, New = prop.CurrentValue }; } else if (entry.State == EntityState.Modified && prop.IsModified) { changes[prop.Metadata.Name] = new { Old = prop.OriginalValue, New = prop.CurrentValue }; } changes[prop.Metadata.Name] = new { Old = prop.OriginalValue, New = prop.CurrentValue }; else if (entry.State == EntityState.Deleted) { changes[prop.Metadata.Name] = new { Old = prop.OriginalValue, New = (object?)null }; } } if (changes.Count == 0) continue; logs.Add(new AuditLog { TableName = tableName, EntityId = keyValue, Action = entry.State.ToString().ToUpper(), AppUserId = userId, CorrelationId = correlationId, CreatedAt = DateTime.UtcNow, ChangesJson = JsonSerializer.Serialize(changes) }); //// Update AuditableEntity fields //if (entry.Entity is AuditableEntity auditable && // entry.State == EntityState.Modified) //{ // auditable.LastModifiedBy = userId; // auditable.LastModifiedUtc = DateTime.UtcNow; //} } return logs; return changes; } }
src/models/Area.cs +1 −1 Original line number Diff line number Diff line namespace Cals.Visor.Models; public class Area : ICreatedAt, IUpdatedAt public class Area : ICreatedAt, IUpdatedAt, IAuditableEntity { public long? AreaId { get; set; } Loading
src/models/InfobloxDomain.cs +1 −1 Original line number Diff line number Diff line namespace Cals.Visor.Models; public class InfobloxDomain : ICreatedAt, IUpdatedAt public class InfobloxDomain : ICreatedAt, IUpdatedAt, IAuditableEntity { public long? InfobloxDomainId { get; set; } Loading
src/models/InfobloxGroup.cs +1 −1 Original line number Diff line number Diff line namespace Cals.Visor.Models; public class InfobloxGroup : ICreatedAt public class InfobloxGroup : ICreatedAt, IAuditableEntity { public long? InfobloxGroupId { get; set; } Loading