(Un)deleting me softly.

Introduction

There are occasions where you want that a delete of record (and the underlying object graph) does not result in a physical delete. Instead, the record (and the underlying object graph) is marked as deleted by means of a dedicated field “IsDeleted”, which gets the value true. This mechanism is called a soft delete.

Beth Massi has a great post on this subject (http://blogs.msdn.com/b/bethmassi/archive/2011/11/18/using-the-save-and-query-pipeline-to-archive-deleted-records.aspx). We’ll elaborate futher on this post.

The requirements

  • we want to be able to apply a soft delete and make sure that the soft delete is propagated automatically to the underlying object graph;
  • we want that entities can optionally participate in the soft delete ‘framework’, but if they participate, compile time checking will ensure that the entity has the “IsDeleted” field;
  • we want to be able in a simple manner to activate filtering in such a way that entities which are soft deleted are not shown;
  • we want to be able to “soft-undelete” an entity and -optionally-  the underlying object graph. It make sense to make this feature permission dependent. Obviously, the usage of this feature goes together with de-activating the filtering on softdleleted items (in other words: when you want to undelete something, you need to see it).

The demo setting

To be able to test things in a decent manner, we make sure the object graph for our test setting is rich enough.

 

 

As you can see, we have both 1 to many structures as well as 1 to 0..1 relations.

How do we use the soft delete base infrastructure?

 

public partial class ApplicationDataService
    {
        partial void Customers_Filter(ref Expression<Func<Customer, bool>> filter)
        {
            filter = filter.NoSoftDeletesFilter<Customer>();
        }

        partial void Customers_Deleting(Customer entity)
        {
            entity.ApplySoftDelete();
        }
        partial void Orders_Deleting(Order entity)
        {
            entity.ApplySoftDelete();
        }

        partial void CustomerAddressDatas_Deleting(CustomerAddressData entity)
        {
            entity.ApplySoftDelete();
        }

        partial void OrderLInes_Deleting(OrderLIne entity)
        {
            entity.ApplySoftDelete();
        }
    }

So, server side, soft-delete for the above object graph is introduced with a few extension method calls.

The undelete functionaly is triggered client side:

 partial void UndoSoftDelete_Execute()
        {
            this.Customers.SelectedItem.UndoSoftDelete();
        }

 

Each entity type you want to let participate in the soft delete engine, should implement the following interface:

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

The base infrastructure

Following 3 extension methods make up the soft delete engine:

Make sure to include following class in the common project !

 

public static class SoftDeleteExtensions
    {
        public static Expression<Func<T, bool>> NoSoftDeletesFilter<T>(this Expression<Func<T, bool>> filter) where T : IIsSoftDeletable
        {
            ParameterExpression paramExpr = Expression.Parameter(typeof(T), "item");
            PropertyInfo isDeletedPropInfo = typeof(T).GetProperty("IsDeleted", BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Instance);
            MemberExpression isDeletedPropExpr = Expression.Property(paramExpr, isDeletedPropInfo);
            ConstantExpression isTrue = Expression.Constant(true, typeof(bool));
            BinaryExpression isNotDeleted = Expression.NotEqual(isDeletedPropExpr, isTrue);
            return (Expression.Lambda<Func<T, bool>>(isNotDeleted, paramExpr));
        }

        public static void ApplySoftDelete(this IEntityObject entity)
        {
            if (entity is IIsSoftDeletable)
            {
                entity.Details.DiscardChanges();
                entity.Details.Properties["IsDeleted"].Value = true;
            }
            else
            {
                throw new ArgumentException("You are trying to apply a filtering on soft deleted items, but the entity does not have an 'IsDeleted' field");
            }
        }

        public static void UndoSoftDelete(this IEntityObject entity, bool recurse = false)
        {
            if (!TrySetIsDeletedProperty(entity))
                return;

            if (recurse)
            {
                IEnumerable<INavigationPropertyDefinition> navigationProperties = entity.Details.GetModel().NavigationProperties;
                foreach (INavigationPropertyDefinition item in navigationProperties)
                {
                    IAssociationEndDefinition associationEnd = item.Association.Ends.Where(e => e.EntityType.Name == entity.Details.EntitySet.Details.EntityType.Name).FirstOrDefault();
                    if (associationEnd != null)
                    {
                        var deleteRuleAttribute = associationEnd.Attributes.OfType<IDeleteRuleAttribute>().FirstOrDefault();
                        if (deleteRuleAttribute != null && deleteRuleAttribute.Action == DeleteAction.Cascade)
                        {
                            string navigationPropName = item.Name;
                            if (item.ToEnd.Multiplicity == AssociationEndMultiplicity.Many)
                            {
                                foreach (var navigationPropValue in (entity.Details.Properties[navigationPropName].Value as IEnumerable<IEntityObject>))
                                {
                                    UndoSoftDelete(navigationPropValue);
                                }
                            }
                            else
                            {
                                UndoSoftDelete(entity.Details.Properties[navigationPropName].Value as IEntityObject);
                            }
                        }
                    }
                }
            }
        }

        private static bool TrySetIsDeletedProperty(IEntityObject entity)
        {
            bool result = false;
            if (entity is IIsSoftDeletable)
            {
                entity.Details.Properties["IsDeleted"].Value = false;
                result = true;
            }
            else
            {
                throw new ArgumentException("You are trying to apply a soft delete undo, but the entity does not have an 'IsDeleted' field");
            }
            return result;
        }
    }

 

As you can see, the softdelete filter is pretty advanced and is a combination of expression tree handling and reflective coding. I got some help of Justin Anderson via the LightSwitch forum (http://social.msdn.microsoft.com/Forums/en-US/lightswitch/thread/ff9c5bf4-001b-4d38-84b7-06af906eff14).

Note also the undo delete functionality is optionally recursive and if active  we traverse the complete object graph.