I implemented soft deletion in EF Core using an IInterceptor:
IEnumerable<EntityEntry<ISoftDeletable>> entries =
eventData
.Context
.ChangeTracker
.Entries<ISoftDeletable>()
.Where(e => e.State == EntityState.Deleted);
foreach (EntityEntry<ISoftDeletable> softDeletable in entries)
{
softDeletable.State = EntityState.Modified;
softDeletable.Property("IsDeleted").CurrentValue = true;
}
But I run into a problem where I can remove any entity and logical consistency of the data is not always preserved.
For example I can remove document but all document lines will not be removed. Firstly I tried to set IsDeleted also for every dependent entity, but I don't know how to do that generically (for every possible reference without explicitly specifying them).
Also ideally, I want it to be consistent with EF Core OnDelete configuration, so for example if I cannot physically delete a record from the database (because it has dependent records) I shouldn't be able to soft delete that record either.
Is there a way to do that, or do I want too much?
Update:
I've done couple tests and found out that when cascade ondelete is set the code working as I want, ef core gladly set all other records that are referenciong on the object as deleted as long as they are tracked.
Implementing something like a cascading soft-delete using interceptors or overriding deletes in the DbContextisn't easy. My approach for soft-delete is via more DDD-based actions rather than via the DbContext. I do have an interceptor, but it's job is to throw an exception if a ISoftDelete entity is in the Deleted state. So my Document entity would have a Delete action which marks the IsDeleted and timstamp. While this could manage the cascading in a tidier way, regardless of the approach you will run into the issue that even if you resolve which children need to be marked for deletion, the entity and DbContext are only aware of children that happen to be loaded at the time. Either approach only works if Document.DocumentLines is eager loaded. Simply checking if the collection is empty or not is error prone, as is assuming they are loaded. Documents may have no lines, or the lines were not eager loaded but partially filled with lines that were already tracked and automatically linked when fetching the Document. That last scenario would result in only some of the lines being marked as deleted with others left active, and it would be intermittent/situational at runtime, leaving corrupt data and tough to reproduce.
For normal parent child relationships where children are only relevant with their parent, I am not concerned with cascading the IsDeleted down to all of the children. I would leave the DocumentLines "active" even though their Document is marked as deleted. Aside from the difficulty and cost in ensuring the DocumentLines are eager loaded when the document is deleted, or relying on unattractive solutions like triggers (which can mess up in situations where a child is in the tracking cache) there is a situation with soft-delete and recovery that works better if you don't cascade through to children.
For instance I have a document with 3 lines. At some point I mark document line #2 as deleted, so the document has Lines #1 and #3 active. I go to delete that document. If I am using Soft-delete for recover-ability (un-delete) if I cascade delete the lines, when I restore that document, Lines 1,2, and 3 would be restored by default. If I don't cascade then when the document is restored, it's lines would return to the state they were when the document was deleted, with 1 & 3 active, 2 remaining inactive. Personally I find this is the better behavior as the Line's delete state is preserved and not overridden by a parent.
The downside of this is if for any reason I do need to query across children, that query must consider the parent's active state alongside the child's active state. This is honestly a rarer situation than dealing with children via their parents so I opt to add this requirement into the querying where it is needed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With