When developing applications that handle sensitive data or need to maintain a history of records, physically deleting data may not be the best approach. In these cases, Soft Delete emerges as a sleek and effective solution. In this article, we will explore what Soft Delete is, how to implement it using Entity Framework, and practical examples to illustrate how it works.
What is Soft Delete?
Soft Delete is a data management technique that involves logically marking records as “deleted” instead of removing them physically from the database. In other words, instead of permanently deleting, you use a flag to indicate that the record is no longer active but still exists in the system. This is often done through a boolean column like “IsDeleted” or “Deleted” to indicate the record’s status.
Usefulness of Soft Delete
Using Soft Delete offers several advantages:
- Data Recovery: It allows for restoring accidentally deleted data.
- Operation History: It keeps a record of deletions, which can be useful for audits and activity tracking.
- Referential Integrity: It helps maintain referential integrity of the database, avoiding issues with broken foreign keys.
Traditional Deletion
Traditional deletion removes the record physically from the database, so it cannot be recovered later. This approach is simple and direct, but it is not suitable for scenarios where recovering deleted data or maintaining a history is necessary.
To illustrate traditional record deletion with Entity Framework, let’s consider the following Product entity:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
A traditional delete method first needs to locate the record in the database, and then use the Remove method from the DbSet or DbContext, passing the record as a parameter. Afterward, to persist the operation, we invoke the SaveChanges method from the DbContext:
public void DeleteProduct(int id)
{
var product = _context.Products.Find(id);
if (product != null)
{
_context.Products.Remove(product);
_context.SaveChanges();
}
}
When using the DeleteProduct method, the equivalent row will be removed from the database table, making it impossible to recover later.
Implementing Soft Delete
Implementing Soft Delete with Entity Framework involves a few main steps. First, we need to add a column to the Product entity that represents the deletion status:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsDeleted { get; set; }
}
Here, “IsDeleted” is a boolean field that indicates whether the entity was deleted with “true” or not with “false.” If your entity didn’t already have an equivalent property, you would need to update the database table to create the new column, which can be done using migrations.
Update Instead of Deletion
One possible approach is to replace the traditional deletion logic with an update that marks the record as deleted. To do this, we set the “IsDeleted” property to true, indicating that the record was logically deleted. In this case, the deletion method would look like this:
public void DeleteProduct(int id)
{
var product = _context.Products.Find(id);
if (product != null)
{
product.IsDeleted = true;
_context.SaveChanges();
}
}
Note that, compared to the traditional deletion method, we essentially removed the call to the Remove method and added the change to the IsDeleted property.
Another way to implement Soft Delete is by overriding the “SaveChanges” method of the DbContext. In this approach, we intercept all deletion operations and transform them into updates that set the “IsDeleted” property to “true” before they are saved to the database. This ensures that any attempt to delete a record results in it being marked as deleted, rather than being physically removed.
public override int SaveChanges()
{
foreach (var entry in ChangeTracker.Entries())
{
if (entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
}
}
return base.SaveChanges();
}
The first step we took was to intercept all deletion operations. This was done using the Entity Framework’s “ChangeTracker,” which tracks all changes made to entities. Next, we filtered the entries marked for deletion (State == EntityState.Deleted) that belong to the Product entity. For each entity marked for deletion, we changed its state to “Modified” and set the “IsDeleted” property to “true.” Finally, we called the “SaveChanges” method from the base class to save the changes to the database.
By using this approach, we no longer need to modify the IsDeleted property in the delete method. In fact, this method will remain traditional, using Remove to perform the deletion. However, when SaveChanges is called, this call will be intercepted and altered in the DbContext.
Using a global query filter
One of the main precautions when using soft delete is ensuring that deleted products are not listed in the application (unless necessary for business reasons). This can be done easily by adding a condition in the Where method, for example:
var products = _context.Products.Where(x => !x.IsDeleted);
However, ensuring that this filter is applied consistently across all parts of the system can be challenging and, if it fails, could lead to serious business consequences (for example, if a customer is able to purchase a product that was mistakenly marked as deleted).
Fortunately, Entity Framework offers the global query filter feature, which, as the name suggests, allows us to apply a condition to the listing of an entity that will be respected throughout the entire application. This is done in the entity mapping, as shown below:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity().HasQueryFilter(p => !p.IsDeleted);
base.OnModelCreating(modelBuilder);
}
With this configuration, all queries for the “Product” entity will automatically exclude products marked as deleted, maintaining the integrity and simplicity of queries throughout the application.
Ignoring the Global Filter
If there’s a specific need to query products marked as deleted, you can temporarily bypass the global filter using the IgnoreQueryFilters() method in a specific query. For example, if we want to implement a method that returns a product by its ID, even if it has been deleted, we can do the following:
var product = _context.Products.IgnoreQueryFilters()
.FirstOrDefault(p => p.Id == id);
In the code above, IgnoreQueryFilters() is called before FirstOrDefault(), ensuring that the query ignores the global filters and includes products that have been logically deleted (i.e., where IsDeleted is true). This allows us to retrieve the product by its ID, regardless of its deletion status.
Conclusion
Soft Delete with Entity Framework is an effective approach to manage records securely and efficiently in applications. By adopting this technique, you can ensure that your data is maintained with integrity and ease of recovery, meeting both security and auditing requirements. Implementing Soft Delete may require additional adjustments depending on the complexity of your system, but the benefits in terms of data management typically outweigh the initial implementation effort.