Files
YouTube-Manager/Manager.Data/Contexts/AuditInterceptor.cs

112 lines
4.0 KiB
C#

using System.Text.Json;
using System.Text.Json.Serialization;
using Manager.Data.Entities.Audit;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Manager.Data.Contexts;
public class AuditInterceptor : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
AddAudit(eventData.Context);
return base.SavingChanges(eventData, result);
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
AddAudit(eventData.Context);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
private void AddAudit(DbContext? context)
{
if (context == null) return;
var entries = context.ChangeTracker.Entries()
.Where(e => e.State is EntityState.Modified or EntityState.Deleted or EntityState.Added && Attribute.IsDefined(e.Entity.GetType(),
typeof(AuditableAttribute)));
var audits = new List<EntityAudit>();
foreach (var entry in entries)
{
var primaryKey = entry.Properties.First(p => p.Metadata.IsPrimaryKey()).CurrentValue?.ToString();
var declaredProperties = entry.Entity.GetType()
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(p => !Attribute.IsDefined(p.DeclaringType!, typeof(NoAuditAttribute), false))
.Select(p => p.Name)
.ToHashSet();
var allowedProperties = entry.Properties.Where(p => declaredProperties.Contains(p.Metadata.Name));
switch (entry.State)
{
case EntityState.Added:
audits.AddRange(allowedProperties
.Where(p => p.CurrentValue != null)
.Select(p => CreateAudit(entry, p, entry.State, primaryKey))
);
break;
case EntityState.Modified:
audits.AddRange(allowedProperties
.Where(p => p.IsModified)
.Select(p => CreateAudit(entry, p, entry.State, primaryKey))
);
break;
case EntityState.Deleted:
audits.AddRange(allowedProperties
.Select(p => CreateAudit(entry, p, entry.State, primaryKey))
);
break;
}
}
if (audits.Count != 0)
{
context.Set<EntityAudit>().AddRange(audits);
}
}
private EntityAudit CreateAudit(EntityEntry entry, PropertyEntry prop, EntityState changeType, string? primaryKey)
{
return new EntityAudit
{
Id = Guid.NewGuid(),
EntityName = entry.Entity.GetType().Name,
EntityId = primaryKey ?? "Unknown",
PropertyName = prop.Metadata.Name,
OldValue = SerializeValue(prop.OriginalValue),
NewValue = SerializeValue(prop.CurrentValue),
ModifiedUtc = DateTime.UtcNow,
ChangedBy = "SYSTEM",
ChangeType = changeType
};
}
private readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private string? SerializeValue(object? value)
{
if (value == null) return null;
var type = value.GetType();
if (type.IsPrimitive || type == typeof(string) || type == typeof(DateTime) || type == typeof(decimal))
{
return value.ToString();
}
return JsonSerializer.Serialize(value, _jsonSerializerOptions);
}
}