Skip to content

Commit

Permalink
feat : Add soft deletion support and global query filters (#1051)
Browse files Browse the repository at this point in the history
Updated `AuditableEntity` and `ISoftDeletable` to include `Deleted` and `DeletedBy` properties for soft deletion tracking. Modified `FshDbContext` to apply a global query filter for `ISoftDeletable` entities, ensuring deleted entities are excluded from queries. Enhanced `AuditInterceptor` to handle soft deletions, including setting `Deleted` and `DeletedBy` properties and updating entity states. Added `AppendGlobalQueryFilter` extension method to facilitate the application of global query filters to entities implementing specific interfaces.

Co-authored-by: Mukesh Murugan <[email protected]>
  • Loading branch information
AliRafay and iammukeshm authored Nov 22, 2024
1 parent e7b5514 commit df50461
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 15 deletions.
2 changes: 2 additions & 0 deletions src/api/framework/Core/Domain/AuditableEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public class AuditableEntity<TId> : BaseEntity<TId>, 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<Guid>
Expand Down
3 changes: 2 additions & 1 deletion src/api/framework/Core/Domain/Contracts/ISoftDeletable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

public interface ISoftDeletable
{

DateTimeOffset? Deleted { get; set; }
Guid? DeletedBy { get; set; }
}
Original file line number Diff line number Diff line change
@@ -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<TInterface>(this ModelBuilder modelBuilder, Expression<Func<TInterface, bool>> 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;
}
}
6 changes: 6 additions & 0 deletions src/api/framework/Infrastructure/Persistence/FshDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ public class FshDbContext(IMultiTenantContextAccessor<FshTenantInfo> 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<ISoftDeletable>(s => s.Deleted == null);
base.OnModelCreating(modelBuilder);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.EnableSensitiveDataLogging();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ private async Task PublishAuditTrailsAsync(DbContextEventData eventData)
var utcNow = timeProvider.GetUtcNow();
foreach (var entry in eventData.Context.ChangeTracker.Entries<IAuditable>().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
};

Expand Down Expand Up @@ -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;
}
Expand All @@ -106,9 +114,9 @@ public void UpdateEntities(DbContext? context)
if (context == null) return;
foreach (var entry in context.ChangeTracker.Entries<AuditableEntity>())
{
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();
Expand All @@ -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;
}
}
}
}
Expand Down

0 comments on commit df50461

Please sign in to comment.