Tuesday 20 January 2009

LINQ to Entities and the “Collection was modified” exception – A solution!

I’ve found that something that is a common mistake when dealing with collections.  Say I want to go through a list of objects and delete every one that matches a certain criteria.  The simplest code would be something like this:

foreach (Item item in _Entities.Item)
{
    if (/* criteria */)
    {
        _Entities.Item.Remove(item);
    }
}

But that’s not going to work.  When the Remove method is run you will get an exception:

{"Collection was modified; enumeration operation may not execute."}    System.Exception {System.InvalidOperationException}

Ok, so if you think about it it makes sense really, you’re trying to remove an item from the enumeration that you’re currently enumerating through.  But what’s the solution?

Turns out there are many ways you can tackle this problem.  One way is to write each of your delete segments so that you build a list of items to delete first and then delete them from the master list.  It works but it’s a lot of extra code.  Another option is to turn the Colleciton into an array before looping through it and deleting the items.

I like this solution better:

public void RemoveIf<T>(ICollection<T> collection, Predicate<T> match)
{
    List<T> removed = new List<T>();
    foreach (T item in collection)
    {
        if (match(item))
        {
            removed.Add(item); 
        }
    }
 
    foreach (T item in removed)
    {
        collection.Remove(item);
    }
 
    removed.Clear();
}

I’ve written my own helper method to remove an item if it meets a certain criteria.  The helper method will take a Predicate (the same as the Collection.Exists method) and if an items in the collection match the Predicate it will add them to a list, then cycle through the list and remove them.

To implement it use code similar to the following:

RemoveIf(_Entities.Item, delegate(Item i) { return /* criteria */; });

Which is the same style of code you would use if you were trying to implement the Collection.Exists method.  If you wanted to delete all the items from one list if they exist in another, you can of course chain the Exists method into the RemoveIf helper also like so:

RemoveIf(_Entities.Item, delegate(Item itemRemove) 
{
    return !otherList.Exists(delegate(Item itemSearch) 
    { 
        return itemSearch.CustomerID == itemRemove.CustomerID; 
    });
});

But the downside is that the code will get a little confusing for people who haven’t used delegates much, or at all.

3 comments:

Marcus said...

Here's how I did it by chaining a couple of entity framework queries to extract those not required, which I then removed. The new list is required to prevent the exception you detailed above.

private void RemoveSalesProduct(Entities db, Store store, ICollection products)
{
var unrequired = new List(store.Product.Where(x => products.All(y => y != x.ProductID)));
foreach (var product in unrequired)
{
store.Product.Remove(db.Product.First(p => p.ProductID == product.ProductID));
}
}

Anonymous said...

Linq-based Suggestion:

entity.Colletion.ForEach(s => /* modifications */)
entity.Save();

IEnumerable ForEach impl:

public static void ForEach(this IEnumerable enumerable, Action action)
{
foreach (var item in enumerable)
action.Invoke(item);
}

David said...

Or I found this neat way of doing it (not sure how efficient though):

foreach (Item item in _Entities.Item.ToList())

{

if (/* criteria */)

{

_Entities.Item.Remove(item);

}

}