Collection enumeration and InvalidOperationException

Posts   
 
    
obzekt
User
Posts: 60
Joined: 29-Apr-2004
# Posted on: 04-Jan-2007 22:59:05   

Hello. Consider a relation Customer and Orders (1:n) and this code:

foreach (OrderEntity oe in customer.Orders)
{
     oe.SetNewFieldValue((int)OrderFieldIndex.CustomerID, null);
     oe.Save();
}

Now the above used to work fine but now throws InvalidOperationException from MoveNext (Collection was modified; enumeration operation may not execute) with the latest version of LLBL that uses generics, after the first iteration. Is that by design and why that breaking change? What do you suggest to do as workaround?

bclubb
User
Posts: 934
Joined: 12-Feb-2004
# Posted on: 05-Jan-2007 02:23:27   

This is how the enumerators with collections work. Here's a quick link on it http://msdn2.microsoft.com/en-us/library/system.collections.ienumerator.movenext.aspx.

I believe this would work if you use a for loop instead of foreach since it won't use MoveNext.

obzekt
User
Posts: 60
Joined: 29-Apr-2004
# Posted on: 05-Jan-2007 08:17:30   

The MoveNext exception is correct. What is strange is that the orders collection obtained before changing the FK of one of each orders to null, changes afterwards and its Count is decremented. This is new behavior with LLB2 and I want to know if it is by design. In the older version, an entity had to be explicitly removed from the collection, which is more logical in my opinion.

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39613
Joined: 17-Aug-2003
# Posted on: 05-Jan-2007 10:30:36   

It is indeed by design and new behavior in v2.0 (new as in: compared with 1.0.2005.1)

IMHO it's logical what is happening in v2.0, as an order which doesn't reference a given customer object, shouldn't be in an Orders collection of said customer.

Frans Bouma | Lead developer LLBLGen Pro
obzekt
User
Posts: 60
Joined: 29-Apr-2004
# Posted on: 05-Jan-2007 14:44:44   

My understanding is that the collections were designed as simple read-only containers of data, a snapshot of a query at a given point. That's why an entity is not deleted from the DB when removed from the collection. I remember this was confusing to many users in the past but it was a good thing I believe. Now it seems as if the collections try to be 'smart' and in sync with the DB state of their contained entities which creates ambiguity. It is actually counter-intuitive to delete an entity with FKs that don't cascade, since you can't iterate the collection. You have to do something like:

int count = customer.Orders.Count;
for (int i = 0; i < count; i++)
{
    OrderEntity oe = customer.Orders[0];
    oe.SetNewFieldValue(OrderFieldIndex.CustomerID, null);
    oe.Save();
}
customer.Delete();

Or maybe there is a more elegant way?

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39613
Joined: 17-Aug-2003
# Posted on: 05-Jan-2007 15:22:11   

obzekt wrote:

My understanding is that the collections were designed as simple read-only containers of data, a snapshot of a query at a given point. That's why an entity is not deleted from the DB when removed from the collection. I remember this was confusing to many users in the past but it was a good thing I believe. Now it seems as if the collections try to be 'smart' and in sync with the DB state of their contained entities which creates ambiguity. It is actually counter-intuitive to delete an entity with FKs that don't cascade, since you can't iterate the collection. You have to do something like:

int count = customer.Orders.Count;
for (int i = 0; i < count; i++)
{
    OrderEntity oe = customer.Orders[0];
    oe.SetNewFieldValue(OrderFieldIndex.CustomerID, null);
    oe.Save();
}
customer.Delete();

Or maybe there is a more elegant way?

I disagree. The thing is that the SYNC code adds the entity to the collection, so doing: myOrder.Customer = myCustomer; adds myOrder to myCustomer.Orders; If you then do: myOrder.Customer = null; // or set it to another customer it's nothing more than logical to remove myOrder from myCustomer.Orders.

We had a lot of requests to change this in the way it is now, so we did.

To overcome this, you can simply do: List<OrderEntity> toProcess = new List<OrderEntity>(); toProcess.AddRange(customer.Orders); foreach(OrderEntity o in toProcess) { // do your work here; }

Frans Bouma | Lead developer LLBLGen Pro
obzekt
User
Posts: 60
Joined: 29-Apr-2004
# Posted on: 05-Jan-2007 15:51:14   

I understand. Now what if you have an OrderCollection of new entities and you want to add them to a customer? Will this work:

foreach (OrderEntity oe in coll)
    oe.Customer = myCustomer;

or will 'coll' drop the entities after they get added to myCustomer.Orders?

Is that breaking change documented? It seems I missed it, and now need to review lotsa code cry

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39613
Joined: 17-Aug-2003
# Posted on: 05-Jan-2007 18:47:43   

obzekt wrote:

I understand. Now what if you have an OrderCollection of new entities and you want to add them to a customer? Will this work:

foreach (OrderEntity oe in coll)
    oe.Customer = myCustomer;

or will 'coll' drop the entities after they get added to myCustomer.Orders?

You can simply do myCustomer.Orders.AddRange(coll);

and the Customer property of the order instances will be set as well.

Is that breaking change documented? It seems I missed it, and now need to review lotsa code cry

In the what's new -> runtime libraries:

Synching setup of entities related to eachother has been enhanced, so that also dereferencing through FK field changes will result in proper clean-up of synchronization data and references.

Though that's easily missed.

Rule of thumb: if you are doing foreach over a collection, don't change values in the object you're consuming which you pulled from the collection, in that case either use a for loop or use an intermediate list. I always use an intermediate list if I want to use a foreach statement, as it's 100% correct: a forloop also can give you bad indexes.

Frans Bouma | Lead developer LLBLGen Pro