Bulk Delete only for loaded entities

Update: I made a new version here.


In my previous post, I showed you how to do Bulk Update with EF4. What was very cool with my solution is the fact that we don’t have to load entities to delete them. But sometimes, we want to delete only already loaded entities.


So I add a new method DeleteLoadedEntities:


public interface IObjectContextWithBulkOperations
{
    void Delete<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate)
        where T : class, TBase
        where TBase : class;
    void DeleteLoadedEntities<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate)
        where T : class, TBase
        where TBase : class;
}

public static class ObjectSetExtension
{
    public static void Delete<TBase, T>(this ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate)
        where T : class, TBase
        where TBase : class
    {
        IObjectContextWithBulkOperations context = entitySet.Context as IObjectContextWithBulkOperations;
        if (context == null)
            throw new NotImplementedException();
        context.Delete(entitySet, predicate);
    }
    public static void DeleteLoadedEntities<TBase, T>(this ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate)
        where T : class, TBase
        where TBase : class
    {
        IObjectContextWithBulkOperations context = entitySet.Context as IObjectContextWithBulkOperations;
        if (context == null)
            throw new NotImplementedException();
        context.DeleteLoadedEntities(entitySet, predicate);
    }
    public static void Delete<T>(this ObjectSet<T> entitySet, Expression<Func<T, bool>> predicate)
        where T : class
    {
        Delete<T, T>(entitySet, predicate);
    }
    public static void DeleteLoadedEntities<T>(this ObjectSet<T> entitySet, Expression<Func<T, bool>> predicate)
        where T : class
    {
        DeleteLoadedEntities<T, T>(entitySet, predicate);
    }
}

And now my ObjectContextWithBulkOperations is the following:


public class ObjectContextWithBulkOperations : ObjectContext, IObjectContextWithBulkOperations
{
    public ObjectContextWithBulkOperations(EntityConnection connection)
        : base(connection)
    {
        OnContextCreated();
    }
    public ObjectContextWithBulkOperations(string connectionString)
        : base(connectionString)
    {
        OnContextCreated();
    }
    protected ObjectContextWithBulkOperations(EntityConnection connection, string defaultContainerName)
        : base(connection, defaultContainerName)
    {
        OnContextCreated();
    }
    protected ObjectContextWithBulkOperations(string connectionString, string defaultContainerName)
        : base(connectionString, defaultContainerName)
    {
        OnContextCreated();
    }

    private void OnContextCreated()
    {
        ObjectMaterialized += NorthwindEntities_ObjectMaterialized;
    }

    private List<Action> _bulkDeletedActions;
    private List<Action> BulkDeletedActions
    {
        get
        {
            if (_bulkDeletedActions == null)
                _bulkDeletedActions = new List<Action>();
            return _bulkDeletedActions;
        }
    }

    private List<object> _bulkDeletedEntities;
    public List<object> BulkDeletedEntities
    {
        get
        {
            if (_bulkDeletedEntities == null)
                _bulkDeletedEntities = new List<object>();
            return _bulkDeletedEntities;
        }
    }

    private Dictionary<Type, List<Func<object, bool>>> _bulkDeletedFuncs;
    public Dictionary<Type, List<Func<object, bool>>> BulkDeletedFuncs
    {
        get
        {
            if (_bulkDeletedFuncs == null)
                _bulkDeletedFuncs = new Dictionary<Type, List<Func<object, bool>>>();
            return _bulkDeletedFuncs;
        }
    }

    public void Update<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate, Expression<Action<T>> set)
        where T : class, TBase
        where TBase : class
    {
    }
    public void UpdateLoadedEntities<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate, Expression<Action<T>> set)
        where T : class, TBase
        where TBase : class
    {
    }

    public void Delete<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate)
        where T : class, TBase
        where TBase : class
    {
        Delete<TBase, T>(entitySet, predicate, true);
    }
    public void DeleteLoadedEntities<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate)
        where T : class, TBase
        where TBase : class
    {
        Delete<TBase, T>(entitySet, CalculatePredicate(entitySet, predicate), false);
    }

    private Expression<Func<T, bool>> CalculatePredicate<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> oldPredicate)
        where T : class, TBase
        where TBase : class
    {
        IEnumerable<PropertyInfo> keyMembers = entitySet.EntitySet.ElementType.KeyMembers.Select(km => typeof(T).GetProperty(km.Name)).ToList();
        IEnumerable<T> entitiesEnumerable = ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified | EntityState.Unchanged).Select(ose => ose.Entity)
            .OfType<T>();
        ParameterExpression parameter = oldPredicate.Parameters.Single();
        if (!entitiesEnumerable.Any())
            return e => false;
        return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(oldPredicate.Body, entitiesEnumerable.Select(e => keyMembers.Select(km => Expression.Equal(Expression.MakeMemberAccess(parameter, km),
            Expression.Constant(km.GetValue(e, null)))).Aggregate((accumulate, clause) => Expression.AndAlso(accumulate, clause))).Aggregate((accumulate, clause) => Expression.OrElse(accumulate, clause))),
            oldPredicate.Parameters);
    }

    private void Delete<TBase, T>(ObjectSet<TBase> entitySet, Expression<Func<T, bool>> predicate, bool propagateToFutureEntities)
        where TBase : class
        where T : class, TBase
    {
        ObjectQuery<T> objectQuery = (ObjectQuery<T>)entitySet.OfType<T>().Where(predicate);
        string selectSQLQuery = objectQuery.ToTraceString();
        string from = Regex.Match(selectSQLQuery, "FROM [\\[A-Za-z0-9\\] .]+\\] AS").Value;
        from = from.Substring(0, from.Length - 3);
        IEnumerator<EdmMember> keyMembersEnumerator = entitySet.EntitySet.ElementType.KeyMembers.GetEnumerator();
        StringBuilder joinClause = new StringBuilder();
        keyMembersEnumerator.MoveNext();
        for (; ; )
        {
            joinClause.Append("MMExtent.");
            joinClause.Append(keyMembersEnumerator.Current);
            joinClause.Append(" = MMExtent2.");
            joinClause.Append(keyMembersEnumerator.Current);

            if (keyMembersEnumerator.MoveNext())
                joinClause.Append(" AND ");
            else
                break;
        }
        BulkDeletedActions.Add(() => ExecuteStoreCommand(string.Format("DELETE MMExtent {0} AS MMExtent INNER JOIN ({1}) AS MMExtent2 ON {2}", from, objectQuery.ToTraceString().Replace("@p__linq__", "@p"),
            joinClause.ToString()), objectQuery.Parameters.Select(p => p.Value).ToArray()));
        Func<T, bool> predicateCompiled = predicate.Compile();
        Func<object, bool> predicateCompiledObject = o =>
        {
            T t = o as T;
            if (t == null)
                return false;
            return predicateCompiled(t);
        };
        if (propagateToFutureEntities)
        {
            List<Func<object, bool>> bulkDeletedFuncs;
            if (BulkDeletedFuncs.TryGetValue(typeof(TBase), out bulkDeletedFuncs))
                bulkDeletedFuncs.Add(predicateCompiledObject);
            else
                BulkDeletedFuncs.Add(typeof(TBase), new List<Func<object, bool>>() { predicateCompiledObject });
        }
        foreach (var entity in ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified | EntityState.Unchanged).Select(ose => ose.Entity).OfType<T>()
            .Where(e => predicateCompiled(e)))
        {
            DeleteObject(entity);
            BulkDeletedEntities.Add(entity);
        }
    }

    private void NorthwindEntities_ObjectMaterialized(object sender, ObjectMaterializedEventArgs e)
    {
        List<Func<object, bool>> bulkDeletedFuncs;
        if (_bulkDeletedFuncs != null)
        {
            Type t = e.Entity.GetType();
            do
            {
                if (BulkDeletedFuncs.TryGetValue(t, out bulkDeletedFuncs))
                    foreach (Func<object, bool> bulkDeletedFunc in bulkDeletedFuncs)
                        if (bulkDeletedFunc(e.Entity))
                        {
                            ObjectStateManager.GetObjectStateEntry(e.Entity).Delete();
                            BulkDeletedEntities.Add(e.Entity);
                            return;
                        }
            } while ((t = t.BaseType) != null);
        }
    }

    public override int SaveChanges(SaveOptions options)
    {
        int value;
        using (TransactionScope transaction = new TransactionScope())
        {
            if (_bulkDeletedEntities != null)
                foreach (object entity in _bulkDeletedEntities)
                {
                    ObjectStateEntry ose;
                    if (ObjectStateManager.TryGetObjectStateEntry(entity, out ose))
                        Detach(entity);
                }
            bool acceptChanges = (options & SaveOptions.AcceptAllChangesAfterSave) == SaveOptions.AcceptAllChangesAfterSave;
            if (acceptChanges)
                options ^= SaveOptions.AcceptAllChangesAfterSave;
            value = base.SaveChanges(options);
            if (_bulkDeletedActions != null)
                foreach (Action action in _bulkDeletedActions)
                    action();
            transaction.Complete();
            if (acceptChanges)
                AcceptAllChanges();
        }
        return value;
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        if (disposing)
            ObjectMaterialized -= NorthwindEntities_ObjectMaterialized;
    }
}

Hope that helps


Matthieu

This entry was posted in 7671, 7674. Bookmark the permalink.

2 Responses to Bulk Delete only for loaded entities

  1. Marcus Coelho says:

    I was looking for something like this few weeks ago.
    Actually, I was wondering if you have some solution that allow you to set the database in a generated SELECT statement, like change a “select * from production.product” to “select * from adventureworks.production.product”.
    Thanks anyway.

  2. Matthieu MEZIL says:

    I think it should be easier if you change the context connection string instead of changing the SQL query.
    Matthieu

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>