C# – EF 4.1 and “Collection was modified; enumeration operation may not execute.” exception

centity-framework-4.1

This has been driving me nuts for the last 2 days.
I have 3 pretty basic classes (well, reduced for readability)

public class Employee 
{
    public string Name { set; get; }
    virtual public Employer Employer { set; get; }

    public Employee(string name)
    {
        this.Name = name;
    }
}

,

// this basically ties Employee and his role in a company.
public class EmployeeRole{
    public int Id { set; get; }
    virtual public Employee Employee { set; get; }
    public string Role { set; get; }

    public EmployeeRole(Employee employee, string role){
        this.Employee = employee;
        this.Role = role;
    }
}

and

public class Employer{

    public string Name { set; get; }
    List<EmployeeRole> employees = new List<EmployeeRole>();
    virtual public List<EmployeeRole> Employees { get { return this.employees; } }

    public Employer(string name, Employee creator){
        this.Name = name;
        this.Employees.Add(new EmployeeRole(creator, "Creator"));
        creator.Employer = this;
    }
}

Seems pretty simple. Not doing any specific configuration for those classes in DbContext.

But, when I run following code

using (DbContext db = DbContext.GetNewDbContext()){

    Employee creator = new Employee("Bob");
    db.Employees.Add(creator);
    db.SaveChanges();

    Employer employer = new Employer("employer", creator);
    db.Employers.Add(employer);
    db.SaveChanges();
   // I know I can call SaveChanges once (and it actually works in this case), 
   // but I want to make sure this would work with saved entities.
}

It throws following exception:

Collection was modified; enumeration operation may not execute.

Stack trace:

at System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource
resource) at
System.Collections.Generic.List1.Enumerator.MoveNextRare() at
System.Collections.Generic.List
1.Enumerator.MoveNext() at
System.Data.Objects.ObjectStateManager.PerformAdd(IList1 entries)
at
System.Data.Objects.ObjectStateManager.AlignChangesInRelationships(IList
1
entries) at System.Data.Objects.ObjectStateManager.DetectChanges()
at System.Data.Objects.ObjectContext.DetectChanges() at
System.Data.Entity.Internal.InternalContext.DetectChanges(Boolean
force) at
System.Data.Entity.Internal.Linq.InternalSet1.ActOnSet(Action action,
EntityState newState, Object entity, String methodName) at
System.Data.Entity.Internal.Linq.InternalSet
1.Add(Object entity)
at System.Data.Entity.DbSet`1.Add(TEntity entity)

Anyone has an idea what's going on and maybe how to fix it ?
Thanks !

Best Answer

For me this looks like a bug in Entity Framework. I've cooked down your example to a simpler one but with the same structure:

public class TestA // corresponds to your Employee
{
    public int Id { get; set; }
    public TestB TestB { get; set; } // your Employer
}

public class TestB // your Employer
{
    public TestB()
    {
        TestCs = new List<TestC>();
    }

    public int Id { get; set; }
    public ICollection<TestC> TestCs { get; set; } // your EmployeeRoles
}

public class TestC // your EmployeeRole
{
    public int Id { get; set; }
    public TestA TestA { get; set; } // your Employee
}

These are three entities with cyclic relationships:

TestA -> TestB -> TestC -> TestA

If I use now a corresponding code with the same structure than yours I get the same exception:

var testA = new TestA();
var testB = new TestB();
var testC = new TestC();

context.TestAs.Add(testA);

testA.TestB = testB;
testB.TestCs.Add(testC);
testC.TestA = testA;

context.ChangeTracker.DetectChanges();

Note that I have used DetectChanges instead of SaveChanges because the stack trace in the exception makes clear that actually DetectChanges causes the exception (which is called internally by SaveChanges). I also found that calling SaveChanges twice is not the problem. The problem here is only the "early" adding to the context before the whole object graph is completed.

The collection which was modified (as the exception is complaining about) is not the TestB.TestCs collection in the model. It seems to be a collection of entries in the ObjectStateManager. I could verify this by replacing ICollection<TestC> TestCs with a single reference by TestC TestC in the TestB class. This way the model doesn't contain any collection at all but it still throws the same exception about a modified collection. (SaveChanges will fail though with three single references because EF doesn't know in which order to save the entities due to the cycle. But that is another problem.)

I would consider it as a bug that EF change detection (DetectChanges) seems to modify its own internal collection it is just iterating through.

Now, the fix to this problem is easy: Just Add entities to the context as your last step before you call SaveChanges:

var testA = new TestA();
var testB = new TestB();
var testC = new TestC();

testA.TestB = testB;
testB.TestCs.Add(testC);
testC.TestA = testA;

context.TestAs.Add(testA);

context.ChangeTracker.DetectChanges();

EF will add the whole related object graph to the context. This code succeeds (also using SaveChanges instead of DetectChanges).

Or your example:

using (DbContext db = DbContext.GetNewDbContext()){
    Employee creator = new Employee("Bob");
    Employer employer = new Employer("employer", creator);

    db.Employees.Add(creator);
    db.SaveChanges();
}

Edit

This was the same exception: Entity Framework throws "Collection was modified" when using observable collection. Following the code in that example the situation was similar: Adding an entity to the context and then afterwards changing/adding relationships to that entity.

Edit2

Interestingly this throws the same exception:

var testA = context.TestAs.Find(1); // assuming there is already one in the DB
var testB = new TestB();
var testC = new TestC();

testA.TestB = testB;
testB.TestCs.Add(testC);
testC.TestA = testA;

context.SaveChanges(); // or DetectChanges, it doesn't matter

So, I want to add relationships with new entities to the existing entity. A fix to this problem seems to be less obvious.

Related Topic