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)
{
///
/// Exposes the database facade for provider-specific operations.
///
public new DatabaseFacade Database => base.Database;
///
/// Applies Prefab conventions in addition to consumer-provided model configuration.
///
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(IPrefabDb).Assembly);
PrefabOnModelCreating(modelBuilder);
modelBuilder.IgnoreDomainEvents();
}
///
/// Allows derived contexts to extend model configuration.
///
/// The model builder supplied by EF Core.
protected abstract void PrefabOnModelCreating(ModelBuilder builder);
///
/// Applies Prefab defaults followed by consumer configuration hooks.
///
/// The options builder used to configure the context.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
PrefabOnConfiguring(optionsBuilder);
}
///
/// Allows derived contexts to customise provider-specific options.
///
/// The options builder supplied by EF Core.
protected abstract void PrefabOnConfiguring(DbContextOptionsBuilder optionsBuilder);
///
/// Persists changes to the database while executing auditing and outbox processing.
///
public override int SaveChanges() => SaveChangesAsync().ConfigureAwait(false).GetAwaiter().GetResult();
///
/// Persists changes to the database while executing auditing and outbox processing.
///
/// Token used to cancel the save operation.
public override async Task 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();
var propertiesCache = new Dictionary();
var auditEntries = new List();
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()
};
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()
.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.DeletedOn));
var deletedByProp = entityType.GetProperty(nameof(EntityWithAuditAndStatus.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.DeletedOn);
if (entry.Metadata.FindProperty(deletedOnPropertyName) is not null)
{
entry.Property(deletedOnPropertyName).IsModified = true;
}
const string deletedByPropertyName = nameof(EntityWithAuditAndStatus.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;
}
///
/// Gets the audit log set.
///
public DbSet AuditLogs { get; set; } = null!;
///
/// Gets the audit log item set.
///
public DbSet AuditLogItems { get; set; } = null!;
///
/// Gets the generic attribute set.
///
public DbSet GenericAttributes => Set();
///
/// Gets the seeder log set.
///
public DbSet SeederLogs => Set();
}