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(); }