Search code examples
c#entity-framework-4

How do I prevent orphaned objects in Entity Framework 4.x?


I have the following (pseudo-)code... the actual code is much more complex, but this is the gist of the root issue:

    string customXML = GoFetchCustomXML();
    using (MyContext ctx = new MyContext(...))
    {
        SomeTable tbl = CreateEntryInTable(customXML);

        ctx.SomeTables.AddObject(tbl);

        ctx.SaveChanges();
    }


...

public SomeTable CreateEntryInTable(string customXML)
{
    XDocument doc = XDocument.Parse(customXML);
    SomeTable ret = new SomeTable();

    foreach (XElement descendant in doc.Descendants("ChildObject").ToList())
    {
        ChildTable ct = new ChildTable();

        // Set some initial items about ct based on
        // customer configurations. It sets our StatusCodeID to "NEW".
        initializeCT(ct, SomeGlobalCustomerObject);

        if (ValidateChildObject(descendant, ct))
        {
            // Set final ct properties here. We move the 
            // StatusCodeID to "Valid" among many other things.

            // Before we go on, set CreateDate
            ct.CreateDate = DateTime.Now;

            ret.ChildTables.AddObject(ct);
        } else {
            // Do nothing. We've changed our mind about needing 
            // a ChildTable object.
        }
    }

    return ret;
}

I've spent over 8 hours today chasing down a very strange issue with this. I was getting a mystery error: The element at index 0 in the collection of objects to refresh is in the added state. Objects in this state cannot be refreshed.

When I run the code, on the third loop of "descendant" it doesn't pass validation, so it is never added to ChildTables. This should be valid, right? (If not, please let me know!)

HOWEVER -- as I've sorely discovered -- it has been added to the context somehow. It's identity column is in fact "0". When the program gets to "SaveChanges()", it crashes because the date on the record (create date) is 00/00/0001, which is not valid at the database. When I put profiler on the connection, I see that its StatusCodeID == NEW... but this record was never completed and never added to the CTX object or to ChildTables.

What's worse, is that now that this is in this state, the context is toast. I can't find this record to kill it, and I can't save anything because it is an orphaned record somewhere in the context.

If I skip the "invalid" object or rewrite my code such that the object is not created until we are CERTAIN that it will really be needed and added to ChildTables, then it works. But what I've done above should be legal, shouldn't it? If not, can someone explain why?


Solution

  • I found the answer, or at least a very significant work-around.

    1) Don't create the object unless I'm fairly sure that I'm going to need to add them. I could rework ValidateChildObject to take the Descendant and SomeCustomerObject to determine if it was valid and only create and initializeCT if the rules pass.

    2) HOWEVER, there are cases when this isn't feasible in the current design as it would slow things down -- whatever I have to do to validate some settings, I would have to do again in order to set those values in initializeCT. In those cases, as in the "ELSE" clause above, I need to do:

    ...
    } 
    else 
    {
        // Remove it from the ChildTables anyway, just in case 
        // it was magically added. If it was not added, this does not fail.
        ctx.ChildTables.DeleteObject(ct);
    }
    

    When I do one of these two approaches, my code runs smoothly.