Rebinding object graph to EntityContext: "unable to track multiple objects with the same key"

Could EF really be that bad? May be,...

Let's say I have a fully loaded, disabled object graph that looks like this:

myReport = 
{Report}
  {ReportEdit {User: "JohnDoe"}}
  {ReportEdit {User: "JohnDoe"}}
      

Basically a report with two changes made by the same user.

And then I do this:

EntityContext.Attach(myReport);
      

InvalidOperationException . An object with the same key already exists in the ObjectStateManager. ObjectStateManager cannot track multiple objects with the same key.

Why? Since EF is trying to connect the {User: "JohnDoe"}

TWICE object .

This will work:

myReport =
{Report}
  {ReportEdit {User: "JohnDoe"}}

EntityContext.Attach(myReport);
      

There is no problem here since the object {User: "JohnDoe"}

only appears in the object graph.

What's more, since you have no control over how EF attaches an object, there is no way to stop it from binding the entire object graph. So really if you want to rewire a complex object containing more than one reference to the same object ... well, good luck.

At least it seems to me. Any comments?

UPDATE: Added example code:


// Load the report 
Report theReport;
using (var context1 = new TestEntities())
{
    context1.Reports.MergeOption = MergeOption.NoTracking;
    theReport = (from r in context1.Reports.Include("ReportEdits.User")
                 where r.Id == reportId
                 select r).First();
}

// theReport looks like this:
// {Report[Id=1]}
//   {ReportEdit[Id=1] {User[Id=1,Name="John Doe"]}
//   {ReportEdit[Id=2] {User[Id=1,Name="John Doe"]}

// Try to re-attach the report object graph
using (var context2 = new TestEntities())
{
    context2.Attach(theReport); // InvalidOperationException
}

      

+2


a source to share


2 answers


The problem is that you changed the default MergeOption

:

context1.Reports.MergeOption = MergeOption.NoTracking;

      



Objects retrieved with NoTracking

are intended to be read-only because there is no fix; this is in the documentation for MergeOption

. Since you installed NoTracking

, you now have two completely separate copies {User: "JohnDoe"}

; without committing "duplicate" links are not limited to a single instance.

Now, when you try to save "both" copies {User: "JohnDoe"}

, the first one is successfully added to the context, but the second cannot be added due to a key violation.

+4


a source


After reading the EF documentation again (v4 stuff is better than 3.5 stuff) and reading this post I figured out the problem - and working around.

With MergeOption.NoTracking

EF creates an object graph where each entity reference is a separate object instance . So in my example, both custom references to 2 ReportEdits are different objects, even though their properties are all the same. They are both in the Separate state, and they both have EntityKeys with the same value.

The problem is that when using the Attach method on the ObjectContext, the context binds each user instance on the basis that they are separate instances - it ignores the fact that they have the same EntityKey .

This behavior makes sense, I suppose. If the objects are in a disabled state, EF doesn't know if one of the two references has changed, etc. Therefore, instead of assuming that they are both immutable and treated as equal, we get an InvalidOperationException.



But what if, as in my case, you know that both user links in disconnected state are actually the same and want to be treated as equal when they were re-linked? It turns out that the solution is simple enough: If an object references the chart multiple times, each of those references must point to one instance of the object .

Using IEntityWithRelationships

, we can traverse the graph of individual objects and update the links and merge duplicate links to the same object instance. The ObjectContext then treats any duplicate entity references as the same object and reattaches it without any error.

Based on the blog post posted above, I created a class to consolidate duplicate object references so that they use the same object reference. Keep in mind that if any of the duplicated references were changed while disabled, you will get unpredictable results: the first object found in the graph always takes precedence. However, in specific scenarios, this seems like a trick.


public class EntityReferenceManager
{
    /// 
    /// A mapping of the first entity found with a given key.
    /// 
    private Dictionary _entityMap;

    /// 
    /// Entities that have been searched already, to limit recursion.
    /// 
    private List _processedEntities;

    /// 
    /// Recursively searches through the relationships on an entity
    /// and looks for duplicate entities based on their EntityKey.
    /// 
    /// If a duplicate entity is found, it is replaced by the first
    /// existing entity of the same key (regardless of where it is found 
    /// in the object graph).
    /// 
    /// 
    public void ConsolidateDuplicateRefences(IEntityWithRelationships ewr)
    {
        _entityMap = new Dictionary();
        _processedEntities = new List();

        ConsolidateDuplicateReferences(ewr, 0);

        _entityMap = null;
        _processedEntities = null;
    }

    private void ConsolidateDuplicateReferences(IEntityWithRelationships ewr, int level)
    {
        // Prevent unlimited recursion
        if (_processedEntities.Contains(ewr))
        {
            return;
        }
        _processedEntities.Add(ewr);

        foreach (var end in ewr.RelationshipManager.GetAllRelatedEnds())
        {
            if (end is IEnumerable)
            {
                // The end is a collection of entities
                var endEnum = (IEnumerable)end;
                foreach (var endValue in endEnum)
                {
                    if (endValue is IEntityWithKey)
                    {
                        var entity = (IEntityWithKey)endValue;
                        // Check if an object with the same key exists elsewhere in the graph
                        if (_entityMap.ContainsKey(entity.EntityKey))
                        {
                            // Check if the object reference differs from the existing entity
                            if (_entityMap[entity.EntityKey] != entity)
                            {
                                // Two objects with the same key in an EntityCollection - I don't think it possible to fix this... 
                                // But can it actually occur in the first place?
                                throw new NotSupportedException("Cannot handle duplicate entities in a collection");
                            }
                        }
                        else
                        {
                            // First entity with this key in the graph
                            _entityMap.Add(entity.EntityKey, entity);
                        }
                    }
                    if (endValue is IEntityWithRelationships)
                    {
                        // Recursively process relationships on this entity
                        ConsolidateDuplicateReferences((IEntityWithRelationships)endValue, level + 1);
                    }
                }
            }
            else if (end is EntityReference) 
            {
                // The end is a reference to a single entity
                var endRef = (EntityReference)end;
                var pValue = endRef.GetType().GetProperty("Value");
                var endValue = pValue.GetValue(endRef, null);
                if (endValue is IEntityWithKey)
                {
                    var entity = (IEntityWithKey)endValue;
                    // Check if an object with the same key exists elsewhere in the graph
                    if (_entityMap.ContainsKey(entity.EntityKey))
                    {
                        // Check if the object reference differs from the existing entity
                        if (_entityMap[entity.EntityKey] != entity)
                        {
                            // Update the reference to the existing entity object
                            pValue.SetValue(endRef, _entityMap[endRef.EntityKey], null);
                        }
                    }
                    else
                    {
                        // First entity with this key in the graph
                        _entityMap.Add(entity.EntityKey, entity);
                    }
                }
                if (endValue is IEntityWithRelationships)
                {
                    // Recursively process relationships on this entity
                    ConsolidateDuplicateReferences((IEntityWithRelationships)endValue, level + 1);
                }
            }
        }
    }
}

      

+3


a source







All Articles