The Fairway Technologies Blog

blog

Soft-Delete As a Cross-Cutting Concern With SharpRepository

In Blog, CSharp, Jeff Treuting, SharpRepository, software development No Comments

Overview

Doing soft deletes is a very common requirement or preference used in software development and it has many benefits including the main one of not accidentally losing data since it is never actually deleted from the database.  The main approach to doing this is to add an IsDeleted flag to the database table and setting that to true instead of doing a hard delete.  Simple enough and it works pretty well.  The annoying part of using this approach comes when you have to add !IsDeleted to every single query you run against each table where you have implemented soft-deletes.  Casey, a fellow Fairway-er (or Fairway-ian, Fairway-ite, ???) pointed this out as we started working on a new project.  So we decided to treat this as a cross-cutting concern and implement it as an attribute that we can apply to the entities that we want to be soft-deleted.  While we are still early on in using this new approach, so far so good.

Note: If you haven't read about SharpRepository Hooks and aspect attributes yet, it might be helpful to check out this blog post first.

Database Structure

This is very straight-forward.  All we will be doing is adding an IsDeleted bit column to our tables that we want to be able to handle soft-deletes.  I've seen IsActive used often, but we decided that IsActive could potentially have a different meaning and be used on the same table where soft-deletes would be happening, so IsDeleted seemed less confusing.

SharpRepository Hooks

As with the auditing hooks I showed in a previous post, there are 3 main parts to the implementation.  First we will have an interface that shows that an entity can be soft deleted, let's call this ISoftDeleteable.

    public interface ISoftDeletable
{
bool IsDeleted { get; set; }
}

 Next, we will decorate our entity with our custom attribute as well as implementing ISoftDeletable.  Again, I'm using Entity Framework so I'll be adding these things to a partial as to not mess with the EF classes created from the database.  Note: the IsDeleted property is already implemented in the main entity class that is generated so adding the ISoftDeleteable just works since the property already exists.

    [SoftDelete]
public partial class MyEntity : ISoftDeletable
{
}

Now to show the main attraction, the SoftDeleteAttribute itself.  This attribute is in charge of a couple things: 1) when Delete is called for an entity that is supposed to be soft-deleted, it will change the delete to an update and set the IsDeleted property to true, and 2) it will add !IsDeleted to every query that is run through the repository for the appropriate entities.  Pretty slick huh.

    public class SoftDeleteAttribute : RepositoryActionBaseAttribute
{
public SoftDeleteAttribute()
{
Order = 99999; // make this go really last by default,
// this is because it will return false to stop the delete from happening, so we don't want it to stop other attributes from running
}

public override bool OnDeleteExecuting<T, TKey>(T entity, RepositoryActionContext<T, TKey> context)
{
var tmp = entity as ISoftDeletable;
if (tmp == null) return true;

// this is a soft delete so let's just update the IsDeleted flag
tmp.IsDeleted = true;

context.Repository.Update((T)tmp);

return false; // don't do the delete
}

public override void OnGetExecuted<T, TKey, TResult>(RepositoryGetContext<T, TKey, TResult> context)
{
var tmp = context.Result as ISoftDeletable;
if (tmp != null && tmp.IsDeleted)
{
context.Result = default(TResult);
}
}

public override bool OnGetAllExecuting<T, TKey, TResult>(RepositoryQueryMultipleContext<T, TKey, TResult> context)
{
context.Specification = UpdateSpecification(context.Specification);

return true;
}

public override bool OnFindAllExecuting<T, TKey, TResult>(RepositoryQueryMultipleContext<T, TKey, TResult> context)
{
context.Specification = UpdateSpecification(context.Specification);

return true;
}

public override bool OnFindExecuting<T, TKey, TResult>(RepositoryQuerySingleContext<T, TKey, TResult> context)
{
context.Specification = UpdateSpecification(context.Specification);

return true;
}

private static ISpecification<T> UpdateSpecification(ISpecification<T> specification)
{
var predicate = BuildSoftDeletePredicate<T>();

if (predicate != null)
{
// need to add in a predicate for IsDeleted == true
return specification == null
? new Specification<T>(predicate)
: specification.And(predicate);
}

return specification;
}

private static readonly IDictionary<Type, object> PredicateCache = new Dictionary<Type, object>();
private static Expression<Func<T, bool>> BuildSoftDeletePredicate<T>()
{
var type = typeof(T);

if (!typeof(ISoftDeletable).IsAssignableFrom(type))
{
return null;
}

if (PredicateCache.ContainsKey(type))
{
return (Expression<Func<T, bool>>)PredicateCache[type];
}

var pe = Expression.Parameter(type, "x");
var property = Expression.Property(pe, "IsDeleted");
var equal = Expression.Equal(property, Expression.Constant(false));
var lambda = Expression.Lambda<Func<T, bool>>(equal, new [] { pe });

PredicateCache[type] = lambda;

return lambda;
}
}

For the OnDeleteExecuting method, you can see that it sets the IsDeleted flag to true, and then makes an Update call on the same repository and cancels the Delete call by returning false.  Maybe in the future we can figure out a cleaner way to do this but it works with the current implementation of aspects within SharpRepository and we haven't run into any issues with it yet.  One thing to note is that if you have logging turned on you will see the call to OnDeleteExecuting, then OnUpdateExecuting, and OnUpdateExecuted and never see a call to OnDeleteExecuted. But that makes sense when you think about it, so I'm fine with that.

The OnGet is handled differently than the other queries so I'll talk through that one first.  You will notice that it is using OnGetExecuted instead of OnGetExecuting, meaning that it is run after the call has been made.  This is because there isn't necessarily a query that we can manipulate.  Depending on the data store being used it may run a LINQ query to get this entity based on the key while for others (like Entity Framework) it will use a built-in lookup method.  Therefore, we need to inspect the result from the Get operation and if the property IsDeleted is true, then we don't want to return it and instead return NULL.

For the other queries we will use the before method and alter the query itself to add a check for IsDeleted == false.  The interesting part is in the BuildSoftDeletePredicate method.  First we need to make sure that the entity is an ISoftDeletable since otherwise it will through an error when it tries to create the lambda expression.  We've implemented a cache to save the time it takes to create the Expression tree each time.  This way we will only incur that cost the first time this type of entity is queried.  The C# Expression building is very powerful and this is really a basic example of it.  The one part that always seems to trip me up is that I forget to include the expression parameter as the second parameter in the Expression.Lambda method call.

Wrap-up

Now that this is in place we have the business rule of doing soft-deletes implemented as a cross-cutting concern so we can still code against our repository doing deletes and then write queries against it without having to know or care about the fact that the rows aren't physically deleted from the table itself.  So far I am liking this approach but again its still early on so that may change.  The only downside I see at the moment is that it is difficult to see that soft-deletes are being used when looking at the code, so new developers to the code might be confused, but then again the fact that it hides that implementation detail is kind of the point of it, so we'll see how that feels in the long run.

New Call-to-action

Sign Up For Our Monthly Newsletter

New Call-to-action