Sunday, December 12, 2010

Soft-Delete and Entity Framework

It is very common to find enterprise applications where entities shouldn't be removed from the database, but just "marked" as deleted. If you are using Entity Framework, you can use the following approach. I hope this post helps you!

Firstly, we need to identify which entities are the ones that should support the soft-delete behavior. In order to do that we can use the following interface:

public interface ISoftDeleteEntity
{
bool Deleted { get; set; }
}
We can use partial classes in order to implement the interface, so there is no need to modify the auto-generated classes.


public partial class Customer : ISoftDeleteEntity
{
}
Finally, we need to override the SaveChanges of the ObjectContext in order to get the deleted entities and run the soft-delete logic.

public partial class DatabaseEntities
{
public override int SaveChanges(SaveOptions options)
{
var deletedEntities = GetDeletedEntities();

SoftDelete(deletedEntities);

return base.SaveChanges(options);
}

private List<ISoftDeleteEntity> GetDeletedEntities()
{
return ObjectStateManager
.GetObjectStateEntries(EntityState.Deleted)
.Select(entry => entry.Entity)
.OfType<ISoftDeleteEntity>()
.ToList();
}

private void SoftDelete(List<ISoftDeleteEntity> deletedEntities)
{
deletedEntities.ForEach(e =>
{
ObjectStateManager.ChangeObjectState(e, EntityState.Modified);
e.Deleted = true;
});
}
}
As you can see in the code snippet, first of all we are obtaining all the entities that are going to be deleted and that implement the ISoftDeleteEntity interface. After that, we are changing their state to modified and setting the Deleted property.

Note: If the changes were not detected yet (WCF DataServices scenario), you should call the DetectChanges method before obtaining the deleted entities. The attached source code shows how to do it.

You can download the sample code from here.

16 comments:

reguapo said...

an what is the difference between do this and make an update?

Jorge Fioranelli said...

Although the result is the same, using this technique you get the following advantages:
- Business Objects / Repositories don't need to know whether the entity should be soft-deleted or not.
- There is less chances of mistakes because there is no code that can delete the object.

moguzalp said...

Good work Jorge, thanks.

mmfoscar said...

Hi Jorge,
Great post btw. Just one question: How would you leave the posibility of having both operations available? With this change implemented we won't be able to actually delete the record, the only way this could happen is if we do not implement the interface on the entity. Any ideas?

Thanks.

Jorge Fioranelli said...

Hi mmfoscar,

It depends on how and who has the knowledge about whether it should be soft-delete or hard-delete.
One option could be to add another property that specifies the type of delete. Something like this:

public interface ICustomDeleteEntity
: ISoftDeleteEntity
{
DeleteType DeleteType { get; set; }
}

public enum DeleteType
{
Soft,
Hard
}

In order to set this property, you can create an extension method for the IDbSet:

public static class DbSetExtensions
{
public static void Remove<TEntity>(this IDbSet<TEntity> dbSet, TEntity entity, DeleteType type) where TEntity : class, ICustomDeleteEntity
{
entity.DeleteType = type;
dbSet.Remove(entity);
}
}

And then you will need to change the GetDeletedEntities implementation to something like this:

private List<ISoftDeleteEntity> GetDeletedEntities()
{
return ObjectStateManager
.GetObjectStateEntries(EntityState.Deleted)
.Select(entry => entry.Entity)
.OfType<ICustomDeleteEntity>()
.Where(entity => entity.DeleteType == DeleteType.Soft)
.Cast<ISoftDeleteEntity>()
.ToList();
}

I haven't run the code, so you will probably need to fix it, but I hope it helps you to understand the idea.

Kind Regards,

Jorge

moguzalp said...

Hi again,

With Jorge's solution:

If you have an entity like

Student
name
classId

then if you call the

MyEntities.Students.DeleteObject(student);

even before call MyEntities.SaveChanges();

You loose all bindings on student, I mean It becomes student.class = null

Thus softdelete only keeps name.

Betty said...

did you come up with a nice approach to filtering out deleted items when loading?

geoffhirst said...

Hi, also have you got an idea for dealing with navigation properties that have been soft deleted?

Betty said...

You can automatically filter deleted items if you use a descriminator

eg. modelBuilder.Entity().Map(m => m.Requires("IsDeleted").HasValue(false));

However this means you can't have a deleted flag on the entity and must set it using raw sql. Disabling the PluralizingTableNameConvention makes it easier to figure out the table name

Martin said...

There is a problem with soft delete which I havent figured out a good way to solve yet.
Lets say you delete an Enity which has a property set to Unique in you database.
When soft deleting this object, it will still be available in the database table and you will not be able to insert a new object into the database with the same unique property.
How do you handle these issues?

Anonymous said...

Add a DeleteDate field to the database. Set it to 1970-01-01 by default. When you soft delete an entity set the DeleteDate to the current date. Then put your unique constraint on the column you require to be unique + the deleted flag + the delete date, like so:

ALTER TABLE XXX ADD CONSTRAINT UC_XXX_UniqueColumnName UNIQUE (UniqueColumnName,Deleted,DeleteDate)

This would allow you to delete the entry multiple times. For example, you could have entries like this:

UniqueColumn:Deleted:DeleteDate
-------------------------------
UniqueValue:False:1970-1-1 (active)
UniqueValue:True:2012-2-2 (deleted on 2/2)
UniqueValue:True:2012-2-4 (deleted on 2/4)

Noel said...
This comment has been removed by the author.
Noel said...

If you are losing foreign key or navigation property data, you should add the entry.ApplyOriginalValues(entry.Entity) to unflag them for deletion.

StackOverFlow Answer

Matias Varini said...

Hi Jorge,

I am having a reference problem when i want to SoftDelete an entity which has a collection of another entity which are already soft deleted.

Any ideas?

Paul Moores said...

Hi

I Have implemented your idea and its great, however, upon removing an entity from the dbContext any foreign key properties to parent entities are set to NULL. If I want to "undelete" I can't. I have tried calling ApplyOriginalValues and entity.CurrentValues.SetValues(entity.OriginalValues) with no joy. Any ideas?

Jose Hndez said...

thanks a lot, had to change a few things to fit my situation yet helped me a lot