Skip to content

Commit df50461

Browse files
AliRafayiammukeshm
andauthored
feat : Add soft deletion support and global query filters (#1051)
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 <31455818+iammukeshm@users.noreply.github.com>
1 parent e7b5514 commit df50461

5 files changed

Lines changed: 74 additions & 15 deletions

File tree

src/api/framework/Core/Domain/AuditableEntity.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ public class AuditableEntity<TId> : BaseEntity<TId>, IAuditable, ISoftDeletable
88
public Guid CreatedBy { get; set; }
99
public DateTimeOffset LastModified { get; set; }
1010
public Guid? LastModifiedBy { get; set; }
11+
public DateTimeOffset? Deleted { get; set; }
12+
public Guid? DeletedBy { get; set; }
1113
}
1214

1315
public abstract class AuditableEntity : AuditableEntity<Guid>

src/api/framework/Core/Domain/Contracts/ISoftDeletable.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
public interface ISoftDeletable
44
{
5-
5+
DateTimeOffset? Deleted { get; set; }
6+
Guid? DeletedBy { get; set; }
67
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Linq.Expressions;
2+
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.EntityFrameworkCore.Query;
4+
5+
namespace FSH.Framework.Infrastructure.Persistence;
6+
7+
internal static class ModelBuilderExtensions
8+
{
9+
public static ModelBuilder AppendGlobalQueryFilter<TInterface>(this ModelBuilder modelBuilder, Expression<Func<TInterface, bool>> filter)
10+
{
11+
// get a list of entities without a baseType that implement the interface TInterface
12+
var entities = modelBuilder.Model.GetEntityTypes()
13+
.Where(e => e.BaseType is null && e.ClrType.GetInterface(typeof(TInterface).Name) is not null)
14+
.Select(e => e.ClrType);
15+
16+
foreach (var entity in entities)
17+
{
18+
var parameterType = Expression.Parameter(modelBuilder.Entity(entity).Metadata.ClrType);
19+
var filterBody = ReplacingExpressionVisitor.Replace(filter.Parameters.Single(), parameterType, filter.Body);
20+
21+
// get the existing query filter
22+
if (modelBuilder.Entity(entity).Metadata.GetQueryFilter() is { } existingFilter)
23+
{
24+
var existingFilterBody = ReplacingExpressionVisitor.Replace(existingFilter.Parameters.Single(), parameterType, existingFilter.Body);
25+
26+
// combine the existing query filter with the new query filter
27+
filterBody = Expression.AndAlso(existingFilterBody, filterBody);
28+
}
29+
30+
// apply the new query filter
31+
modelBuilder.Entity(entity).HasQueryFilter(Expression.Lambda(filterBody, parameterType));
32+
}
33+
34+
return modelBuilder;
35+
}
36+
}

src/api/framework/Infrastructure/Persistence/FshDbContext.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ public class FshDbContext(IMultiTenantContextAccessor<FshTenantInfo> multiTenant
1717
private readonly IPublisher _publisher = publisher;
1818
private readonly DatabaseOptions _settings = settings.Value;
1919

20+
protected override void OnModelCreating(ModelBuilder modelBuilder)
21+
{
22+
// QueryFilters need to be applied before base.OnModelCreating
23+
modelBuilder.AppendGlobalQueryFilter<ISoftDeletable>(s => s.Deleted == null);
24+
base.OnModelCreating(modelBuilder);
25+
}
2026
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
2127
{
2228
optionsBuilder.EnableSensitiveDataLogging();

src/api/framework/Infrastructure/Persistence/Interceptors/AuditInterceptor.cs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@ private async Task PublishAuditTrailsAsync(DbContextEventData eventData)
3838
var utcNow = timeProvider.GetUtcNow();
3939
foreach (var entry in eventData.Context.ChangeTracker.Entries<IAuditable>().Where(x => x.State is EntityState.Added or EntityState.Deleted or EntityState.Modified).ToList())
4040
{
41+
var userId = currentUser.GetUserId();
4142
var trail = new TrailDto()
4243
{
4344
Id = Guid.NewGuid(),
4445
TableName = entry.Entity.GetType().Name,
45-
UserId = currentUser.GetUserId(),
46+
UserId = userId,
4647
DateTime = utcNow
4748
};
4849

@@ -72,19 +73,26 @@ private async Task PublishAuditTrailsAsync(DbContextEventData eventData)
7273
break;
7374

7475
case EntityState.Modified:
75-
if (property.IsModified && property.OriginalValue == null && property.CurrentValue != null)
76+
if (property.IsModified)
7677
{
77-
trail.ModifiedProperties.Add(propertyName);
78-
trail.Type = TrailType.Delete;
79-
trail.OldValues[propertyName] = property.OriginalValue;
80-
trail.NewValues[propertyName] = property.CurrentValue;
81-
}
82-
else if (property.IsModified && property.OriginalValue?.Equals(property.CurrentValue) == false)
83-
{
84-
trail.ModifiedProperties.Add(propertyName);
85-
trail.Type = TrailType.Update;
86-
trail.OldValues[propertyName] = property.OriginalValue;
87-
trail.NewValues[propertyName] = property.CurrentValue;
78+
if (entry.Entity is ISoftDeletable && property.OriginalValue == null && property.CurrentValue != null)
79+
{
80+
trail.ModifiedProperties.Add(propertyName);
81+
trail.Type = TrailType.Delete;
82+
trail.OldValues[propertyName] = property.OriginalValue;
83+
trail.NewValues[propertyName] = property.CurrentValue;
84+
}
85+
else if (property.OriginalValue?.Equals(property.CurrentValue) == false)
86+
{
87+
trail.ModifiedProperties.Add(propertyName);
88+
trail.Type = TrailType.Update;
89+
trail.OldValues[propertyName] = property.OriginalValue;
90+
trail.NewValues[propertyName] = property.CurrentValue;
91+
}
92+
else
93+
{
94+
property.IsModified = false;
95+
}
8896
}
8997
break;
9098
}
@@ -106,9 +114,9 @@ public void UpdateEntities(DbContext? context)
106114
if (context == null) return;
107115
foreach (var entry in context.ChangeTracker.Entries<AuditableEntity>())
108116
{
117+
var utcNow = timeProvider.GetUtcNow();
109118
if (entry.State is EntityState.Added or EntityState.Modified || entry.HasChangedOwnedEntities())
110119
{
111-
var utcNow = timeProvider.GetUtcNow();
112120
if (entry.State == EntityState.Added)
113121
{
114122
entry.Entity.CreatedBy = currentUser.GetUserId();
@@ -117,6 +125,12 @@ public void UpdateEntities(DbContext? context)
117125
entry.Entity.LastModifiedBy = currentUser.GetUserId();
118126
entry.Entity.LastModified = utcNow;
119127
}
128+
if(entry.State is EntityState.Deleted && entry.Entity is ISoftDeletable softDelete)
129+
{
130+
softDelete.DeletedBy = currentUser.GetUserId();
131+
softDelete.Deleted = utcNow;
132+
entry.State = EntityState.Modified;
133+
}
120134
}
121135
}
122136
}

0 commit comments

Comments
 (0)