251 lines
9.2 KiB
C#
251 lines
9.2 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
using Prefab.Data.Entities;
|
|
using Prefab.Domain.Common;
|
|
using Prefab.Handler;
|
|
using System.Reflection;
|
|
|
|
namespace Prefab.Data;
|
|
|
|
public abstract class PrefabDb(DbContextOptions options, IHandlerContextAccessor accessor)
|
|
: DbContext(options)
|
|
{
|
|
/// <summary>
|
|
/// Exposes the database facade for provider-specific operations.
|
|
/// </summary>
|
|
public new DatabaseFacade Database => base.Database;
|
|
|
|
/// <summary>
|
|
/// Applies Prefab conventions in addition to consumer-provided model configuration.
|
|
/// </summary>
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
base.OnModelCreating(modelBuilder);
|
|
|
|
modelBuilder.ApplyConfigurationsFromAssembly(typeof(IPrefabDb).Assembly);
|
|
|
|
PrefabOnModelCreating(modelBuilder);
|
|
modelBuilder.IgnoreDomainEvents();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows derived contexts to extend model configuration.
|
|
/// </summary>
|
|
/// <param name="builder">The model builder supplied by EF Core.</param>
|
|
protected abstract void PrefabOnModelCreating(ModelBuilder builder);
|
|
|
|
/// <summary>
|
|
/// Applies Prefab defaults followed by consumer configuration hooks.
|
|
/// </summary>
|
|
/// <param name="optionsBuilder">The options builder used to configure the context.</param>
|
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
|
{
|
|
PrefabOnConfiguring(optionsBuilder);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows derived contexts to customise provider-specific options.
|
|
/// </summary>
|
|
/// <param name="optionsBuilder">The options builder supplied by EF Core.</param>
|
|
protected abstract void PrefabOnConfiguring(DbContextOptionsBuilder optionsBuilder);
|
|
|
|
/// <summary>
|
|
/// Persists changes to the database while executing auditing and outbox processing.
|
|
/// </summary>
|
|
public override int SaveChanges() => SaveChangesAsync().ConfigureAwait(false).GetAwaiter().GetResult();
|
|
|
|
/// <summary>
|
|
/// Persists changes to the database while executing auditing and outbox processing.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Token used to cancel the save operation.</param>
|
|
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
|
|
{
|
|
var now = DateTimeOffset.UtcNow;
|
|
var correlationId = Guid.TryParse(accessor?.Current?.CorrelationId, out var parsedCorrelationId)
|
|
? parsedCorrelationId
|
|
: Guid.Empty;
|
|
|
|
var auditEntitiesCache = new Dictionary<Type, bool>();
|
|
var propertiesCache = new Dictionary<Type, (PropertyInfo? CreatedBy, PropertyInfo? CreatedOn, PropertyInfo? ModifiedBy, PropertyInfo? ModifiedOn)>();
|
|
var auditEntries = new List<Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry>();
|
|
|
|
foreach (var entry in ChangeTracker.Entries())
|
|
{
|
|
var entityType = entry.Entity.GetType();
|
|
if (!auditEntitiesCache.TryGetValue(entityType, out var entityIsAuditable))
|
|
{
|
|
entityIsAuditable = entityType.GetInterfaces()
|
|
.Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntityWithAudit<>));
|
|
auditEntitiesCache[entityType] = entityIsAuditable;
|
|
}
|
|
|
|
if (!entityIsAuditable)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!propertiesCache.TryGetValue(entityType, out var props))
|
|
{
|
|
props = (
|
|
CreatedBy: entityType.GetProperty(nameof(IAudit.CreatedBy)),
|
|
CreatedOn: entityType.GetProperty(nameof(IAudit.CreatedOn)),
|
|
ModifiedBy: entityType.GetProperty(nameof(IAudit.LastModifiedBy)),
|
|
ModifiedOn: entityType.GetProperty(nameof(IAudit.LastModifiedOn))
|
|
);
|
|
propertiesCache[entityType] = props;
|
|
}
|
|
|
|
switch (entry.State)
|
|
{
|
|
case EntityState.Added:
|
|
props.CreatedBy?.SetValue(entry.Entity, props.CreatedBy.GetValue(entry.Entity) ?? accessor?.Current?.UserId);
|
|
props.CreatedOn?.SetValue(entry.Entity, now);
|
|
auditEntries.Add(entry);
|
|
break;
|
|
case EntityState.Modified:
|
|
props.ModifiedBy?.SetValue(entry.Entity, accessor?.Current?.UserId ?? accessor?.Current?.UserId);
|
|
props.ModifiedOn?.SetValue(entry.Entity, now);
|
|
auditEntries.Add(entry);
|
|
break;
|
|
case EntityState.Deleted:
|
|
if (TrySoftDelete(entry, props, accessor?.Current?.UserId, now))
|
|
{
|
|
entry.State = EntityState.Modified;
|
|
auditEntries.Add(entry);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
foreach (var entry in auditEntries)
|
|
{
|
|
var audit = new AuditLog
|
|
{
|
|
CorrelationId = correlationId,
|
|
Entity = entry.Entity.GetType().Name,
|
|
State = entry.State.ToString(),
|
|
CreatedBy = Guid.TryParse(accessor?.Current?.UserId, out var userId) ? userId : Guid.Empty,
|
|
CreatedOn = now,
|
|
Items = new List<AuditLogItem>()
|
|
};
|
|
|
|
foreach (var prop in entry.Properties)
|
|
{
|
|
if (entry.State == EntityState.Modified && !prop.IsModified)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var oldVal = entry.State == EntityState.Modified ? prop.OriginalValue?.ToString() : null;
|
|
var newVal = prop.CurrentValue?.ToString();
|
|
audit.Items.Add(new AuditLogItem
|
|
{
|
|
Property = prop.Metadata.Name,
|
|
OldValue = oldVal,
|
|
NewValue = newVal
|
|
});
|
|
}
|
|
|
|
AuditLogs.Add(audit);
|
|
}
|
|
|
|
var domainEventEntities = ChangeTracker
|
|
.Entries<IHasDomainEvents>()
|
|
.Where(e => e.Entity.Events.Count > 0)
|
|
.Select(e => e.Entity)
|
|
.ToList();
|
|
|
|
if (domainEventEntities.Count > 0)
|
|
{
|
|
var domainEvents = domainEventEntities.SelectMany(e => e.Events).ToList();
|
|
foreach (var ev in domainEvents)
|
|
{
|
|
//Outbox.Add(Prefab.Queuing.Domain.Outbox.New(ev));
|
|
}
|
|
|
|
domainEventEntities.ForEach(e => e.ClearEvents());
|
|
}
|
|
|
|
return await base.SaveChangesAsync(cancellationToken);
|
|
}
|
|
|
|
private static bool TrySoftDelete(
|
|
Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry entry,
|
|
(PropertyInfo? CreatedBy, PropertyInfo? CreatedOn, PropertyInfo? ModifiedBy, PropertyInfo? ModifiedOn) props,
|
|
string? userId,
|
|
DateTimeOffset deletedOn)
|
|
{
|
|
var entityType = entry.Entity.GetType();
|
|
var supportsSoftDelete = entityType.GetInterfaces()
|
|
.Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntityWithAuditAndStatus<>));
|
|
|
|
if (!supportsSoftDelete)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var deletedOnProp = entityType.GetProperty(nameof(EntityWithAuditAndStatus<Guid>.DeletedOn));
|
|
var deletedByProp = entityType.GetProperty(nameof(EntityWithAuditAndStatus<Guid>.DeletedBy));
|
|
|
|
deletedOnProp?.SetValue(entry.Entity, deletedOn);
|
|
if (deletedByProp is not null)
|
|
{
|
|
if (Guid.TryParse(userId, out var parsedUserId))
|
|
{
|
|
deletedByProp.SetValue(entry.Entity, parsedUserId);
|
|
}
|
|
else
|
|
{
|
|
deletedByProp.SetValue(entry.Entity, null);
|
|
}
|
|
}
|
|
|
|
const string deletedOnPropertyName = nameof(EntityWithAuditAndStatus<Guid>.DeletedOn);
|
|
if (entry.Metadata.FindProperty(deletedOnPropertyName) is not null)
|
|
{
|
|
entry.Property(deletedOnPropertyName).IsModified = true;
|
|
}
|
|
|
|
const string deletedByPropertyName = nameof(EntityWithAuditAndStatus<Guid>.DeletedBy);
|
|
if (entry.Metadata.FindProperty(deletedByPropertyName) is not null)
|
|
{
|
|
entry.Property(deletedByPropertyName).IsModified = true;
|
|
}
|
|
|
|
props.ModifiedOn?.SetValue(entry.Entity, deletedOn);
|
|
if (props.ModifiedBy is not null)
|
|
{
|
|
if (Guid.TryParse(userId, out var parsedUserId))
|
|
{
|
|
props.ModifiedBy.SetValue(entry.Entity, parsedUserId);
|
|
}
|
|
else
|
|
{
|
|
props.ModifiedBy.SetValue(entry.Entity, null);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the audit log set.
|
|
/// </summary>
|
|
public DbSet<AuditLog> AuditLogs { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Gets the audit log item set.
|
|
/// </summary>
|
|
public DbSet<AuditLogItem> AuditLogItems { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Gets the generic attribute set.
|
|
/// </summary>
|
|
public DbSet<GenericAttribute> GenericAttributes => Set<GenericAttribute>();
|
|
|
|
/// <summary>
|
|
/// Gets the seeder log set.
|
|
/// </summary>
|
|
public DbSet<SeederLog> SeederLogs => Set<SeederLog>();
|
|
}
|