diff --git a/src/api/framework/Core/Domain/AuditableEntity.cs b/src/api/framework/Core/Domain/AuditableEntity.cs index f62ad672b..6639a0215 100644 --- a/src/api/framework/Core/Domain/AuditableEntity.cs +++ b/src/api/framework/Core/Domain/AuditableEntity.cs @@ -8,6 +8,8 @@ public class AuditableEntity : BaseEntity, IAuditable, ISoftDeletable public Guid CreatedBy { get; set; } public DateTimeOffset LastModified { get; set; } public Guid? LastModifiedBy { get; set; } + public DateTimeOffset? Deleted { get; set; } + public Guid? DeletedBy { get; set; } } public abstract class AuditableEntity : AuditableEntity diff --git a/src/api/framework/Core/Domain/Contracts/ISoftDeletable.cs b/src/api/framework/Core/Domain/Contracts/ISoftDeletable.cs index b64d5dd57..d129d02e4 100644 --- a/src/api/framework/Core/Domain/Contracts/ISoftDeletable.cs +++ b/src/api/framework/Core/Domain/Contracts/ISoftDeletable.cs @@ -2,5 +2,6 @@ public interface ISoftDeletable { - + DateTimeOffset? Deleted { get; set; } + Guid? DeletedBy { get; set; } } diff --git a/src/api/framework/Infrastructure/Persistence/AppendGlobalQueryFilterExtension.cs b/src/api/framework/Infrastructure/Persistence/AppendGlobalQueryFilterExtension.cs new file mode 100644 index 000000000..dbd09831d --- /dev/null +++ b/src/api/framework/Infrastructure/Persistence/AppendGlobalQueryFilterExtension.cs @@ -0,0 +1,36 @@ +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; + +namespace FSH.Framework.Infrastructure.Persistence; + +internal static class ModelBuilderExtensions +{ + public static ModelBuilder AppendGlobalQueryFilter(this ModelBuilder modelBuilder, Expression> filter) + { + // get a list of entities without a baseType that implement the interface TInterface + var entities = modelBuilder.Model.GetEntityTypes() + .Where(e => e.BaseType is null && e.ClrType.GetInterface(typeof(TInterface).Name) is not null) + .Select(e => e.ClrType); + + foreach (var entity in entities) + { + var parameterType = Expression.Parameter(modelBuilder.Entity(entity).Metadata.ClrType); + var filterBody = ReplacingExpressionVisitor.Replace(filter.Parameters.Single(), parameterType, filter.Body); + + // get the existing query filter + if (modelBuilder.Entity(entity).Metadata.GetQueryFilter() is { } existingFilter) + { + var existingFilterBody = ReplacingExpressionVisitor.Replace(existingFilter.Parameters.Single(), parameterType, existingFilter.Body); + + // combine the existing query filter with the new query filter + filterBody = Expression.AndAlso(existingFilterBody, filterBody); + } + + // apply the new query filter + modelBuilder.Entity(entity).HasQueryFilter(Expression.Lambda(filterBody, parameterType)); + } + + return modelBuilder; + } +} diff --git a/src/api/framework/Infrastructure/Persistence/FshDbContext.cs b/src/api/framework/Infrastructure/Persistence/FshDbContext.cs index f30fc1966..1f3186e3e 100644 --- a/src/api/framework/Infrastructure/Persistence/FshDbContext.cs +++ b/src/api/framework/Infrastructure/Persistence/FshDbContext.cs @@ -17,6 +17,12 @@ public class FshDbContext(IMultiTenantContextAccessor multiTenant private readonly IPublisher _publisher = publisher; private readonly DatabaseOptions _settings = settings.Value; + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // QueryFilters need to be applied before base.OnModelCreating + modelBuilder.AppendGlobalQueryFilter(s => s.Deleted == null); + base.OnModelCreating(modelBuilder); + } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.EnableSensitiveDataLogging(); diff --git a/src/api/framework/Infrastructure/Persistence/Interceptors/AuditInterceptor.cs b/src/api/framework/Infrastructure/Persistence/Interceptors/AuditInterceptor.cs index e933663d7..6c2d819ca 100644 --- a/src/api/framework/Infrastructure/Persistence/Interceptors/AuditInterceptor.cs +++ b/src/api/framework/Infrastructure/Persistence/Interceptors/AuditInterceptor.cs @@ -38,11 +38,12 @@ private async Task PublishAuditTrailsAsync(DbContextEventData eventData) var utcNow = timeProvider.GetUtcNow(); foreach (var entry in eventData.Context.ChangeTracker.Entries().Where(x => x.State is EntityState.Added or EntityState.Deleted or EntityState.Modified).ToList()) { + var userId = currentUser.GetUserId(); var trail = new TrailDto() { Id = Guid.NewGuid(), TableName = entry.Entity.GetType().Name, - UserId = currentUser.GetUserId(), + UserId = userId, DateTime = utcNow }; @@ -72,19 +73,26 @@ private async Task PublishAuditTrailsAsync(DbContextEventData eventData) break; case EntityState.Modified: - if (property.IsModified && property.OriginalValue == null && property.CurrentValue != null) + if (property.IsModified) { - trail.ModifiedProperties.Add(propertyName); - trail.Type = TrailType.Delete; - trail.OldValues[propertyName] = property.OriginalValue; - trail.NewValues[propertyName] = property.CurrentValue; - } - else if (property.IsModified && property.OriginalValue?.Equals(property.CurrentValue) == false) - { - trail.ModifiedProperties.Add(propertyName); - trail.Type = TrailType.Update; - trail.OldValues[propertyName] = property.OriginalValue; - trail.NewValues[propertyName] = property.CurrentValue; + if (entry.Entity is ISoftDeletable && property.OriginalValue == null && property.CurrentValue != null) + { + trail.ModifiedProperties.Add(propertyName); + trail.Type = TrailType.Delete; + trail.OldValues[propertyName] = property.OriginalValue; + trail.NewValues[propertyName] = property.CurrentValue; + } + else if (property.OriginalValue?.Equals(property.CurrentValue) == false) + { + trail.ModifiedProperties.Add(propertyName); + trail.Type = TrailType.Update; + trail.OldValues[propertyName] = property.OriginalValue; + trail.NewValues[propertyName] = property.CurrentValue; + } + else + { + property.IsModified = false; + } } break; } @@ -106,9 +114,9 @@ public void UpdateEntities(DbContext? context) if (context == null) return; foreach (var entry in context.ChangeTracker.Entries()) { + var utcNow = timeProvider.GetUtcNow(); if (entry.State is EntityState.Added or EntityState.Modified || entry.HasChangedOwnedEntities()) { - var utcNow = timeProvider.GetUtcNow(); if (entry.State == EntityState.Added) { entry.Entity.CreatedBy = currentUser.GetUserId(); @@ -117,6 +125,12 @@ public void UpdateEntities(DbContext? context) entry.Entity.LastModifiedBy = currentUser.GetUserId(); entry.Entity.LastModified = utcNow; } + if(entry.State is EntityState.Deleted && entry.Entity is ISoftDeletable softDelete) + { + softDelete.DeletedBy = currentUser.GetUserId(); + softDelete.Deleted = utcNow; + entry.State = EntityState.Modified; + } } } }