Search code examples
ardalis-specification

Ardalis Specification with ISoftDelete


I am implementing a soft delete for our app. To implement soft delete, I am creating ISoftDelete interface and using it to overwrite the default delete behavior of the EF by overwriting SaveChangesAsync for those entities who have implemented the ISoftDelete interface. It works as expected.

Now the problem occurs when I get the data. To ignore soft deleted rows, I need to add where clause to every specification. This is not convenient and prone to error. I want to extend or overwrite specifications to ignore soft-deleted rows from a single point when I get the data for those entities which implement ISoftDelete Interface. Any suggestions?


Solution

  • You can enable this on DbContext level. Here are a few extensions.

    Extension for applying soft delete on save:

    public interface ISoftDelete
    {
        public bool IsDeleted { get; }
    }
    
    public static class DbContextExtensions
    {
        public static void ApplySoftDelete(this DbContext dbContext)
        {
            var entries = dbContext.ChangeTracker.Entries<ISoftDelete>().Where(x => x.State == EntityState.Deleted);
    
            foreach (var entry in entries)
            {
                entry.CurrentValues.SetValues(entry.OriginalValues);
                entry.State = EntityState.Unchanged;
                entry.CurrentValues[nameof(ISoftDelete.IsDeleted)] = true;
    
                var referenceEntries = entry.References.Where(x => x.TargetEntry is not null &&
                                                                   x.TargetEntry.Metadata.IsOwned());
    
                foreach (var targetEntry in referenceEntries.Select(e => e.TargetEntry))
                {
                    targetEntry!.CurrentValues.SetValues(targetEntry.OriginalValues);
                    targetEntry!.State = EntityState.Unchanged;
                }
            }
        }
    }
    

    Then just override the SavechangesAsync as follows

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        this.ApplySoftDelete();
    
        return await base.SaveChangesAsync(cancellationToken);
    }
    

    Extension for applying SoftDelete filter

    public static class ModelBuilderExtensions
    {
        public static void ConfigureSoftDelete(this ModelBuilder modelBuilder)
        {
            foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            {
                if (typeof(ISoftDelete).IsAssignableFrom(entityType.ClrType))
                {
                    modelBuilder.Entity(entityType.Name, x => x.Property(nameof(ISoftDelete.IsDeleted)));
                    entityType.AddSoftDeleteQueryFilter();
                }
            }
        }
    
        private static void AddSoftDeleteQueryFilter(this IMutableEntityType entityData)
        {
            var methodToCall = typeof(ModelBuilderExtensions)
                .GetMethod(nameof(GetSoftDeleteFilter), BindingFlags.NonPublic | BindingFlags.Static)!
                .MakeGenericMethod(entityData.ClrType);
    
            var filter = methodToCall.Invoke(null, Array.Empty<object>());
    
            entityData.SetQueryFilter((LambdaExpression?)filter);
            entityData.AddIndex(entityData.FindProperty(nameof(ISoftDelete.IsDeleted))!);
        }
    
        private static LambdaExpression GetSoftDeleteFilter<TEntity>() where TEntity : class, ISoftDelete
        {
            Expression<Func<TEntity, bool>> filter = x => !x.IsDeleted;
            return filter;
        }
    }
    

    Then override the OnModelCreating in DbContext and add this call.

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ConfigureSoftDelete();
    }