Skip to content

Soft Delete Support

MiCake provides a soft delete feature that allows you to mark data as deleted instead of actually removing it from the database, making data recovery and auditing easy.

Soft Delete is a logical deletion approach that doesn’t actually remove records from the database. Instead, it uses a marker field (such as IsDeleted) to indicate that a record has been deleted.

Advantages:

  • Data can be recovered
  • Complete data history is preserved
  • Easy auditing and tracking
  • Meets data retention requirements in certain industries

Use the UseAudit method to enable the soft delete feature:

// Using AddMiCakeWithDefault
services.AddMiCakeWithDefault<MyAppModule, MyDbContext>(options =>
{
options.AuditConfig = audit =>
{
audit.UseSoftDeletion = true; // Enable soft delete
};
});
// Or use the Builder approach
var builder = services.AddMiCake<MyAppModule>();
builder.UseEFCore<MyDbContext>();
builder.UseAudit(opts =>
{
opts.UseSoftDeletion = true; // Enable soft delete
});

Implement the ISoftDeletable interface:

using MiCake.Audit.SoftDeletion;
public class Product : AggregateRoot<int>, ISoftDeletable
{
public string Name { get; private set; }
public decimal Price { get; private set; }
// Soft delete marker
public bool IsDeleted { get; set; }
private Product() { }
public static Product Create(string name, decimal price)
{
return new Product
{
Name = name,
Price = price,
IsDeleted = false // Not deleted by default
};
}
}

You can use soft delete and the audit feature together:

using MiCake.Audit;
using MiCake.Audit.SoftDeletion;
// Using the combined interface
public class Order : AggregateRoot<int>, IAuditableWithSoftDeletion
{
public string OrderNumber { get; private set; }
// Audit fields
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
// Soft delete fields
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
private Order() { }
}
// Or implement them separately
public class Product : AggregateRoot<int>, IHasAuditTimestamps, ISoftDeletable
{
public string Name { get; private set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public bool IsDeleted { get; set; }
}

Implement the IHasDeletedAt interface to record the deletion time:

using MiCake.Audit.SoftDeletion;
public class Article : AggregateRoot<int>, ISoftDeletable, IHasDeletedAt
{
public string Title { get; private set; }
public string Content { get; private set; }
// Soft delete marker
public bool IsDeleted { get; set; }
// Deletion time
public DateTime? DeletedAt { get; set; }
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
var product = await _productRepository.FindAsync(id);
if (product == null)
return NotFound();
// Soft delete (not actually removed)
await _productRepository.DeleteAsync(product);
// Commit the unit of work (committed automatically at the request boundary in ASP.NET Core)
await _unitOfWork.CommitAsync();
// The IsDeleted field is set to true in the database
// If IHasDeletedAt is implemented, DeletedAt is set to the current time
return Ok();
}

MiCake automatically filters out soft-deleted data:

// Automatically filters out deleted products
public async Task<List<Product>> GetAllProducts()
{
// Only returns products where IsDeleted = false
return await _productRepository.Query()
.ToListAsync();
}
// Query a specific product
public async Task<Product?> GetProduct(int id)
{
// Only non-deleted products can be found
return await _productRepository.FindAsync(id);
}

MiCake automatically configures query filters for entities that implement the ISoftDeletable interface:

using MiCake.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
public class MyDbContext : MiCakeDbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<Order> Orders { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Apply MiCake conventions (including the soft delete filter)
modelBuilder.UseMiCakeConventions();
}
}

MiCake automatically adds global query filters for all entities implementing ISoftDeletable:

// MiCake internal implementation (no need to add manually)
modelBuilder.Entity<Product>()
.HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<Order>()
.HasQueryFilter(e => !e.IsDeleted);
// ✅ Enable soft delete for important data
public class Order : AggregateRoot<int>, ISoftDeletable
{
public bool IsDeleted { get; set; }
}
public class Customer : AggregateRoot<int>, ISoftDeletable
{
public bool IsDeleted { get; set; }
}
// ⚠️ Temporary data such as logs can skip soft delete
public class ApplicationLog : Entity<int>
{
// Doesn't implement ISoftDeletable, can be deleted directly
}
public interface IHasFullDeletion : ISoftDeletable, IHasDeletedAt
{
int? DeleterId { get; set; }
}
public class Order : AggregateRoot<int>, IHasFullDeletion
{
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public int? DeleterId { get; set; }
}

Handling soft delete for related entities:

public class Order : AggregateRoot<int>, ISoftDeletable
{
private readonly List<OrderItem> _items = new();
public bool IsDeleted { get; set; }
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public void SoftDelete()
{
IsDeleted = true;
// Cascade the soft delete to order items
foreach (var item in _items)
{
item.IsDeleted = true;
}
}
}
public class OrderItem : Entity<int>, ISoftDeletable
{
public bool IsDeleted { get; set; }
}

For fields with unique constraints, special handling may be needed after soft delete:

public class User : AggregateRoot<int>, ISoftDeletable
{
public string Email { get; private set; }
public bool IsDeleted { get; set; }
// Configure the unique index in DbContext
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Option 1: The unique index includes the IsDeleted field
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Email, e.IsDeleted })
.IsUnique();
// Option 2: Use a filtered unique index (SQL Server)
modelBuilder.Entity<User>()
.HasIndex(e => e.Email)
.IsUnique()
.HasFilter("[IsDeleted] = 0");
}

Periodically clean up data that has been soft-deleted for a long time:

public class DataCleanupService
{
private readonly MyDbContext _dbContext;
// Clean up data soft-deleted more than 30 days ago
public async Task CleanupOldDeletedData()
{
var thirtyDaysAgo = DateTime.UtcNow.AddDays(-30);
var oldDeletedProducts = await _dbContext.Products
.IgnoreQueryFilters()
.Where(p => p.IsDeleted && p.DeletedAt < thirtyDaysAgo)
.ToListAsync();
_dbContext.Products.RemoveRange(oldDeletedProducts);
await _dbContext.SaveChangesAsync();
}
}
  1. Enable soft delete: The soft delete feature must be explicitly enabled in the configuration (UseSoftDeletion = true)
  2. Query filtering: Soft-deleted data does not appear in query results by default
  3. Include explicitly: Use IgnoreQueryFilters() to include deleted data
  4. Unique constraints: Pay attention to handling fields with unique constraints
  5. Cascade delete: Consider soft delete handling for related entities
  6. Periodic cleanup: Establish a periodic cleanup mechanism to avoid database bloat
  7. Permission control: Restore and permanent delete operations should have appropriate permission control
  8. Convention configuration: Make sure to call modelBuilder.UseMiCakeConventions() in the DbContext to apply the soft delete filter
Interface Fields Description
ISoftDeletable bool IsDeleted Soft delete marker
IHasDeletedAt DateTime? DeletedAt Deletion time
IAuditableWithSoftDeletion Combines audit and soft delete Includes CreatedAt, UpdatedAt, IsDeleted, DeletedAt

Features of the MiCake soft delete feature:

  • Logical deletion: Marks deletion instead of physical deletion, data can be recovered
  • Simple configuration: Implement the interface and enable the configuration to use it
  • Automatic filtering: Deleted data is automatically filtered out in queries
  • Flexible control: You can use IgnoreQueryFilters() to include deleted data
  • Audit integration: Integrates perfectly with the audit feature, recording the deletion time
  • Convention over configuration: Automatically applied through UseMiCakeConventions()

Core steps:

  1. Enable it in the configuration: opts.UseSoftDeletion = true
  2. Implement the interface on the entity: ISoftDeletable or IAuditableWithSoftDeletion
  3. DbContext configuration: call modelBuilder.UseMiCakeConventions()
  4. Use the Repository normally, soft delete takes effect automatically