Introduction
In enterprise applications, data management isn’t just about storing and retrieving records — it’s also about preserving historical context and preventing accidental data loss.
This is where Soft Delete and Auditing come into play.
In this article, we’ll explore how to implement soft delete and auditing in ASP.NET Core using Entity Framework Core, with clean architecture, reusable code, and real-world best practices.
What Is Soft Delete?
Soft delete is a strategy where records are not physically removed from the database.
Instead, they are marked as deleted using a flag (e.g., IsDeleted = true), and excluded from queries automatically.
This helps to:
Prevent data loss by accidental deletion.
Maintain historical integrity.
Allow easy data restoration.
What Is Auditing?
Auditing means tracking who changed what and when — including inserts, updates, and deletes.
For example, you might want to know:
Which user updated an order?
When was a product deleted?
What fields were changed during an update?
Auditing improves traceability, security, and data compliance.
Technical Workflow (Flowchart)
flowchart TD
A[Client Sends Request] --> B[API Controller Calls EF Core]
B --> C[SaveChanges Intercepted]
C --> D{Operation Type?}
D -->|Insert| E[Add CreatedDate, CreatedBy]
D -->|Update| F[Add ModifiedDate, ModifiedBy]
D -->|Delete| G[Mark IsDeleted = true]
G --> H[Skip Physical Deletion]
F --> I[Write Audit Log Entry]
E --> I
I --> J[Save to Database]
J --> K[Response to Client]
Step 1: Define the Base Entity
We’ll create a base class for all entities that need soft delete and auditing fields.
public abstract class BaseEntity
{
public int Id { get; set; }
// Auditing Fields
public string CreatedBy { get; set; }
public DateTime CreatedDate { get; set; }
public string? ModifiedBy { get; set; }
public DateTime? ModifiedDate { get; set; }
// Soft Delete Field
public bool IsDeleted { get; set; }
}
All your entities (e.g., Product, Customer, etc.) will inherit this class.
Step 2: Example Entity Model
public class Product : BaseEntity
{
public string Name { get; set; }
public decimal Price { get; set; }
}
Step 3: Configure Soft Delete Query Filter
In EF Core, we can use a Global Query Filter to automatically exclude soft-deleted records.
public class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Apply global filter for soft delete
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(BaseEntity).IsAssignableFrom(entityType.ClrType))
{
modelBuilder.Entity(entityType.ClrType)
.HasQueryFilter(GetIsDeletedRestriction(entityType.ClrType));
}
}
}
private static LambdaExpression GetIsDeletedRestriction(Type type)
{
var param = Expression.Parameter(type, "e");
var prop = Expression.Property(param, nameof(BaseEntity.IsDeleted));
var condition = Expression.Equal(prop, Expression.Constant(false));
return Expression.Lambda(condition, param);
}
}
✅ Now all EF Core queries automatically exclude records with IsDeleted = true.
Step 4: Override SaveChanges for Auditing and Soft Delete
We’ll intercept EF Core’s SaveChanges method to update audit fields and apply soft delete logic.
public override int SaveChanges()
{
var entries = ChangeTracker.Entries<BaseEntity>();
foreach (var entry in entries)
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedDate = DateTime.UtcNow;
entry.Entity.CreatedBy = "System"; // Replace with logged-in user
break;
case EntityState.Modified:
entry.Entity.ModifiedDate = DateTime.UtcNow;
entry.Entity.ModifiedBy = "System";
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
entry.Entity.ModifiedDate = DateTime.UtcNow;
entry.Entity.ModifiedBy = "System";
break;
}
}
return base.SaveChanges();
}
This ensures that:
Soft delete replaces actual deletion.
Audit fields are updated automatically.
Step 5: Create an Audit Log Table
To record changes in detail, we’ll add an AuditLog entity.
public class AuditLog
{
public int Id { get; set; }
public string TableName { get; set; }
public string KeyValues { get; set; }
public string? OldValues { get; set; }
public string? NewValues { get; set; }
public string Action { get; set; }
public string UserName { get; set; }
public DateTime Timestamp { get; set; }
}
Step 6: Implement Audit Logging in SaveChanges
We can extend our context to log changes before saving them.

Join the conversation! Your thoughts help the community grow.