[CHANGE] Reworked db with interceptors
This commit is contained in:
111
Manager.Data/Contexts/AuditInterceptor.cs
Normal file
111
Manager.Data/Contexts/AuditInterceptor.cs
Normal 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);
|
||||
}
|
||||
}
|
38
Manager.Data/Contexts/DateInterceptor.cs
Normal file
38
Manager.Data/Contexts/DateInterceptor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
Manager.Data/Entities/Audit/AuditableAttribute.cs
Normal file
9
Manager.Data/Entities/Audit/AuditableAttribute.cs
Normal 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
|
||||
{
|
||||
}
|
22
Manager.Data/Entities/Audit/EntityHistory.cs
Normal file
22
Manager.Data/Entities/Audit/EntityHistory.cs
Normal 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; }
|
||||
}
|
9
Manager.Data/Entities/Audit/NoAuditAttribute.cs
Normal file
9
Manager.Data/Entities/Audit/NoAuditAttribute.cs
Normal 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
|
||||
{
|
||||
}
|
@@ -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;
|
||||
|
@@ -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)]
|
||||
|
@@ -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)]
|
||||
|
@@ -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)]
|
||||
|
@@ -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; }
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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)]
|
||||
|
Reference in New Issue
Block a user