[CHANGE] Reworked db with interceptors

This commit is contained in:
max
2025-09-10 23:49:41 +02:00
parent b1e5b0dc68
commit 0f83cf1ddc
23 changed files with 293 additions and 79 deletions

View File

@@ -0,0 +1,111 @@
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)
{
AddHistory(eventData.Context);
return base.SavingChanges(eventData, result);
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
AddHistory(eventData.Context);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
private void AddHistory(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 histories = new List<EntityHistory>();
foreach (var entry in entries)
{
var primaryKey = entry.Properties.First(p => p.Metadata.IsPrimaryKey()).CurrentValue?.ToString() ?? "Unknown";
var declaredProperties = entry.Entity.GetType()
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Where(p => !Attribute.IsDefined(p.DeclaringType!, typeof(NoAuditAttribute)))
.Select(p => p.Name)
.ToHashSet();
var allowedProperties = entry.Properties.Where(p => declaredProperties.Contains(p.Metadata.Name));
switch (entry.State)
{
case EntityState.Added:
histories.AddRange(allowedProperties
.Where(p => p.CurrentValue != null)
.Select(p => CreateHistory(entry, p, entry.State, primaryKey))
);
break;
case EntityState.Modified:
histories.AddRange(allowedProperties
.Where(p => p.IsModified)
.Select(p => CreateHistory(entry, p, entry.State, primaryKey))
);
break;
case EntityState.Deleted:
histories.AddRange(allowedProperties
.Select(p => CreateHistory(entry, p, entry.State, primaryKey))
);
break;
}
}
if (histories.Count != 0)
{
context.Set<EntityHistory>().AddRange(histories);
}
}
private EntityHistory CreateHistory(EntityEntry entry, PropertyEntry prop, EntityState changeType, string? primaryKey)
{
return new EntityHistory
{
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);
}
}

View File

@@ -0,0 +1,38 @@
using Manager.Data.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Manager.Data.Contexts;
public class DateInterceptor : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
UpdateEntryDates(eventData.Context);
return base.SavingChanges(eventData, result);
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
CancellationToken cancellationToken = new())
{
UpdateEntryDates(eventData.Context);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
private void UpdateEntryDates(DbContext? context)
{
if (context == null) return;
var entries = context.ChangeTracker.Entries().Where(x => x is { Entity: DateTimeBase, State: EntityState.Added or EntityState.Modified });
foreach (var entity in entries)
{
((DateTimeBase)entity.Entity).LastModifiedUtc = DateTime.UtcNow;
if (entity.State == EntityState.Added)
{
((DateTimeBase)entity.Entity).CreatedAtUtc = DateTime.UtcNow;
}
}
}
}

View File

@@ -1,4 +1,4 @@
using Manager.Data.Entities;
using Manager.Data.Entities.Audit;
using Manager.Data.Entities.LibraryContext;
using Manager.Data.Entities.LibraryContext.Join;
using Microsoft.EntityFrameworkCore;
@@ -13,6 +13,8 @@ public sealed class LibraryDbContext : DbContext
ChangeTracker.LazyLoadingEnabled = false;
Database.EnsureCreated();
}
public DbSet<EntityHistory> Histories { get; set; }
public DbSet<CaptionEntity> Captions { get; set; }
public DbSet<ChannelEntity> Channels { get; set; }
@@ -21,9 +23,20 @@ public sealed class LibraryDbContext : DbContext
public DbSet<MediaEntity> Media { get; set; }
public DbSet<MediaFormatEntity> MediaFormats { get; set; }
public DbSet<PlaylistEntity> Playlists { get; set; }
// Other media (images)?
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(new DateInterceptor(), new AuditInterceptor());
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<EntityHistory>(eh =>
{
eh.ToTable("entity_history");
});
modelBuilder.Entity<CaptionEntity>(ce =>
{
ce.ToTable("captions");
@@ -101,31 +114,4 @@ public sealed class LibraryDbContext : DbContext
base.OnModelCreating(modelBuilder);
}
public override int SaveChanges()
{
UpdateEntryDates();
return base.SaveChanges();
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new())
{
UpdateEntryDates();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void UpdateEntryDates()
{
var entries = ChangeTracker.Entries().Where(x => x is { Entity: DateTimeBase, State: EntityState.Added or EntityState.Modified });
foreach (var entity in entries)
{
((DateTimeBase)entity.Entity).LastModifiedUtc = DateTime.UtcNow;
if (entity.State == EntityState.Added)
{
((DateTimeBase)entity.Entity).CreatedAtUtc = DateTime.UtcNow;
}
}
}
}

View File

@@ -0,0 +1,9 @@
namespace Manager.Data.Entities.Audit;
/// <summary>
/// Make all properties in the entity audible, if they are changed this will be stored as a history in the db.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class AuditableAttribute : Attribute
{
}

View File

@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace Manager.Data.Entities.Audit;
public class EntityHistory
{
[MaxLength(200)]
public required string EntityName { get; set; }
[MaxLength(200)]
public required string EntityId { get; set; }
[MaxLength(200)]
public required string PropertyName { get; set; }
[MaxLength(1000)]
public string? OldValue { get; set; }
[MaxLength(1000)]
public string? NewValue { get; set; }
public DateTime ModifiedUtc { get; set; } = DateTime.UtcNow;
[MaxLength(200)]
public string? ChangedBy { get; set; }
public EntityState ChangeType { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace Manager.Data.Entities.Audit;
/// <summary>
/// Specifies to ignore the properties in the entity to not audit.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class NoAuditAttribute : Attribute
{
}

View File

@@ -1,5 +1,8 @@
using Manager.Data.Entities.Audit;
namespace Manager.Data.Entities;
[NoAudit]
public abstract class DateTimeBase
{
public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;

View File

@@ -1,7 +1,9 @@
using System.ComponentModel.DataAnnotations;
using Manager.Data.Entities.Audit;
namespace Manager.Data.Entities.LibraryContext;
[Auditable]
public class CaptionEntity : DateTimeBase
{
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]

View File

@@ -1,7 +1,9 @@
using System.ComponentModel.DataAnnotations;
using Manager.Data.Entities.Audit;
namespace Manager.Data.Entities.LibraryContext;
[Auditable]
public class ChannelEntity : DateTimeBase
{
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]

View File

@@ -1,7 +1,9 @@
using System.ComponentModel.DataAnnotations;
using Manager.Data.Entities.Audit;
namespace Manager.Data.Entities.LibraryContext;
[Auditable]
public class ClientAccountEntity : DateTimeBase
{
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]

View File

@@ -1,9 +1,13 @@
using System.ComponentModel.DataAnnotations;
using Manager.Data.Entities.Audit;
namespace Manager.Data.Entities.LibraryContext;
[Auditable]
public class HttpCookieEntity : DateTimeBase
{
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
public required string ClientId { get; set; }
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
public required string Name { get; set; }
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
@@ -15,6 +19,4 @@ public class HttpCookieEntity : DateTimeBase
public DateTimeOffset? ExpiresUtc { get; set; }
public bool Secure { get; set; }
public bool HttpOnly { get; set; }
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
public required string ClientId { get; set; }
}

View File

@@ -1,8 +1,10 @@
using System.ComponentModel.DataAnnotations;
using Manager.Data.Entities.Audit;
using Manager.Data.Entities.LibraryContext.Join;
namespace Manager.Data.Entities.LibraryContext;
[Auditable]
public class MediaEntity : DateTimeBase
{
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]
@@ -17,7 +19,6 @@ public class MediaEntity : DateTimeBase
public List<MediaFormatEntity> Formats { get; set; } = [];
public List<CaptionEntity> Captions { get; set; } = [];
public List<PlaylistMedia> PlaylistMedias { get; set; } = [];
public MediaExternalState ExternalState { get; set; } = MediaExternalState.Online;
public bool IsDownloaded { get; set; }
public MediaState State { get; set; } = MediaState.Indexed;

View File

@@ -1,8 +1,10 @@
using System.ComponentModel.DataAnnotations;
using Manager.Data.Entities.Audit;
using Manager.Data.Entities.LibraryContext.Join;
namespace Manager.Data.Entities.LibraryContext;
[Auditable]
public class PlaylistEntity : DateTimeBase
{
[MaxLength(DataConstants.DbContext.DefaultDbStringSize)]