After reading the EF documentation again (the v4 stuff - it's better than the 3.5 stuff) and reading this post, I realized the issue - and a work around.
With MergeOption.NoTracking
, the EF creates an object graph where each entity reference is a distinct instance of the entity. So in my example, both User references on the 2 ReportEdits are distinct objects - even though all their properties are the same. They are both in the Detached state, and they both have EntityKeys with the same value.
The trouble is, when using the Attach method on the ObjectContext, the context reattaches each User instance based on the fact that they are separate instances - it ignores the fact that they have the same EntityKey.
This behavior makes sense, I suppose. If the entities are in the detached state, the EF doesn't know if one of the two references has been modified, etc. So instead of assuming they are both unchanged and treating them as equal, we get an InvalidOperationException.
But what if, like in my case, you know that both User references in the detached state are in fact the same and want them to be treated as equal when they are reattached? Turns out the solution is simple enough: If an entity is referenced multiple times in the graph, each one of those references needs to point to a single instance of the object.
Using the IEntityWithRelationships
, we can traverse the detached object graph and update the references and consolidate duplicate references to the same entity instance. ObjectContext will then treat any duplicate entity references as the same entity and reattach it without any error.
Based loosely on the blog post I referenced above, I've created a class to consolidate references to duplicate entities so that they share the same object reference. Keep in mind that if any of the duplicate references have been modfied while in the detached state, you'll end up with unpredicatable results: the first entity found in the graph always takes precedence. In specific scenarios though, it seems to do the 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's 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);
}
}
}
}
}