Entity Framework Logical Delete Part Two: Flagging deleted Entities with a naive solution
So in my previous post in the series, I described what soft delete is and what challenges need to be overcome when implementing it using Entity Framework (since EF has no explicit support for this).
Now, in part two, I want to discuss a naive solution — one that might satisfy your needs in less complex cases. The solution is not bad at all in my opinion, but it's less structured, less automated, involves a lot of technical challenges for the more complex cases, and has abstraction leaking written all over it (which is of course not a problem for some people). So let's dive in :)
Implementing soft delete with EF change tracking
So let's say you have this type of model:
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
And then you decide that you want implement soft delete for Person. What do you do?
If you know something about object-orientation, you might introduce an interface like this:
public interface ISoftDelete
{
bool IsDeleted { get; set; }
}
This interface should be implemented by every entity that has soft delete enabled.
public class Person : ISoftDelete
{
public int PersonId { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public bool IsDeleted { get; set; }
}
And you could say that you're done. But of course this way you have to be aware of which entities are soft delete entities and which are not. In case an entity is not a soft delete entity, you can use the .Remove() method as usual. But if it is a soft delete entity, you have to update the IsDeleted flag manually instead of calling .Remove(). This is a solution that has obvious abstraction leaking, which leads to another problem: the solution is not an automated one. So two out of the three technical requirements (identified in the previous post) are broken.
But not all is lost: if you know the change tracking mechanism of Entity Framework, you can implement a more automated solution.
When you call .Remove(), the entity that you pass in as a parameter becomes 'Deleted'. When you call .SaveChanges(), EF looks through its changetracker, filters all the 'Deleted' entites and issues a delete command to the database with the id of the entity. If you override .SaveChanges() (and of course, you also have to override .SaveChangesAsync() accordingly), you can filter out the entities yourself, change the 'Deleted' state to 'Modified' and update the IsDeleted property to true, if the entity is a soft delete entity (for which you can use the interface). This, in turn, will instruct EF to issue and update command instead of a delete command to the database:
public override int SaveChanges()
{
foreach (var item in this.ChangeTracker.Entries())
{
if (item.State == EntityState.Deleted && item.Entity is ISoftDelete)
{
item.CurrentValues[nameof(ISoftDelete.IsDeleted)] = true;
item.State = EntityState.Modified;
}
}
return base.SaveChanges();
}
This is not bad at all. It is maintainable, readable, the right abstractions are in place (you do not need to know if an entity is soft deletable or not when coding the business logic).
One might argue that is has a performance penalty because you have to check all the entries in the changetracker. I have to admit, I've never done any measurements to support or deny this claim with proper data. Also one might argue that this is still leaking some abstraction and this should be up to the database to switch up the operations. There is something to this; I mean, if you don't use the ISoftDelete property in the business logic, then why do you need to add the interface for the business entity. Do you see how the abstraction leaks up?.
These are all good points, which should be considered. But I have found that most of my clients and colleagues like this solution and most of the time don't go into the details of the design. That is, until you need cascade delete.
A few words about cascade delete
Don't get me wrong — it is entirely possible to implement a cascading delete with this solution. All you have to do is explore the EF model for navigation properties and mark them as deleted as well (if they are a soft delete entity) and do this recursively. This is a technical challenge, but I'm pretty sure I could do it if I tried, especially with the myriad of resources on Google :)
But I unfortunately, there's no point in even trying: to mark the entities as deleted, first you have to load them into the memory, if they are not already there. And this makes this solution a bad one: it involves a lot of performance overhead. Loading related entities just for the sake of marking them deleted is a performance penalty that I don't want to pay (especially if we consider the real cascading nature of the cascade delete, where you might have related records to the related records of the related records).
So if we evaluate this solution, we see that it does not support the deletion of related entities well enough (one of the business requirements), it is automated, but has abstraction leaking, and when it comes to cascade deletion, it has a considerable performance penalty. (Note how I have not discussed loading only active records to memory yet; I will do that in separate posts). So all in all, this solution is a 'good enough' solution if you don't have cascade deletes (even with the little bit of abstraction leaking), but let's be honest: you can do better. And when it comes to cascade delete, it all falls apart :( Maybe some stored procedures could help?