Computing an Original Value Graph
In a previous post I shared some code which can be used to compute an original value version of an object using the information stored in ObjectStateManager, and in this post I gave some background about how the state manager and the relationship manager work together to track relationship information. Now let’s see if we can put those things together and extend the code to create an entire original value graph.
The strategy we’ll use is to start by creating an original value version of the object passed in, and then call a private recursive function which will handle the relationships. Whenever this function finds a related entity which has not yet been processed, it will create the original version of it, add that entity to the appropriate reference or collection and then recursively call itself to process the relationships for that entity. This mechanism will address the entire graph, and because whenever we find a related EntityKey we first check to see if that entity is already in the collection or ref, then we can avoid cycles and know that the recursion will terminate.
So how does this look in code? Well, let’s start by building up a few useful helper functions… We already have the function which will create the original value version of a single object. Next we have to deal with the fact that state entries which model relationships store the EntityKeys of the related entities in the first two columns of their data records, but given a key, there’s no way to tell which of the two columns it is in. So, we’ll create a couple of functions—one will return the EntityKey of the “other end” of a relationship given a key that appears on one of the ends, and the other will return the role of the “other end” given a key from one of the ends. We use the OriginalValues record for these because the state entry in question might be deleted in which case it would not have current values, only original ones. The other trick here is that the role name can be extracted from the name of the property where the key for that role appears, and that information lives in the field metadata which is on the DataRecordInfo property of the OriginalValues record. To make things slightly more complicated, the exposed type of the OriginalValues record is DbDataRecord, but the underlying object also implements IExtendedDataRecord, and we have to use that interface to get the DataRecordInfo.
// Given a relationship state entry and a key, return the key of the other end.
private static EntityKey otherEndKey(ObjectStateEntry relationshipEntry, EntityKey thisEndKey)
{
Debug.Assert(relationshipEntry != null);
Debug.Assert(relationshipEntry.IsRelationship);
Debug.Assert(thisEndKey != null);
if ((EntityKey)relationshipEntry.OriginalValues[0] == thisEndKey)
{
return (EntityKey)relationshipEntry.OriginalValues[1];
}
else if ((EntityKey)relationshipEntry.OriginalValues[1] == thisEndKey)
{
return (EntityKey)relationshipEntry.OriginalValues[0];
}
else
{
throw new InvalidOperationException("Neither end of the relationship contains the key.");
}
}
// Given a relationship state entry and a key, return the name of the role of the other end.
private static string otherEndRole(ObjectStateEntry relationshipEntry, EntityKey thisEndKey)
{
Debug.Assert(relationshipEntry != null);
Debug.Assert(relationshipEntry.IsRelationship);
Debug.Assert(thisEndKey != null);
if ((EntityKey)relationshipEntry.OriginalValues[0] == thisEndKey)
{
return ((IExtendedDataRecord)relationshipEntry.OriginalValues)
.DataRecordInfo.FieldMetadata[1].FieldType.Name;
}
else if ((EntityKey)relationshipEntry.OriginalValues[1] == thisEndKey)
{
return ((IExtendedDataRecord)relationshipEntry.OriginalValues)
.DataRecordInfo.FieldMetadata[0].FieldType.Name;
}
else
{
throw new InvalidOperationException("Neither end of the relationship contains the key.");
}
}
Another useful helper method is an extension method for IRelatedEnd which will check to see if that related end object (either an EntityCollection or an EntityReference) already contains the entity for a particular key. We use this when doing the check to bound our recursion. Currently we assume all of the entities implement IEntityWithKey. This assumption is the result of a feature which was inadvertently left out of beta 3. We should be able to remove the assumption after the next release of the EF. Anyway, the method looks like this:
private static bool Contains(this IRelatedEnd relatedEnd, EntityKey key)
{
foreach (object relatedObject in relatedEnd)
{
Debug.Assert(relatedObject is IEntityWithKey);
if (((IEntityWithKey)relatedObject).EntityKey == key)
{
return true;
}
}
return false;
}
Here’s the simple method which kicks off the process:
public static object CreateOriginalValuesGraph(this ObjectContext context, object source)
{
if (source == null)
{
return new ArgumentException("Source parameter must not be null.");
}
object target = context.CreateOriginalValuesObject(source);
IEntityWithRelationships targetWithRelationships = target as IEntityWithRelationships;
if (targetWithRelationships != null)
{
SetRelationships(context, targetWithRelationships);
}
return target;
}
For the real core piece (setting the relationships), there are a few interesting parts…
First, there is the LINQ to Objects query which searches through all of the object state entries which are either unchanged or deleted. We want to model the original version of the graph so we definitely don’t want added entries, and relationship entries are never modified (if you want to change a relationship you delete the old one and create a new one). This query also restricts the set to those which are relationship entries where the EntityKey of the object we are working on appears in one of the two original value columns. Finally, the query groups the entries by the relationship type—that way we can iterate over each relationship on the entity and then over each of the entries for that relationship.
Next, as we iterate over the groups (one for each relationship) we need to retrieve an IRelatedEnd from the RelationshipManager. This interface represents a common way of interacting with the relationship object (collection or reference). We can (thanks to our method above) check if the object for a particular key is already on the related end, and we can add related objects to it. So, once we have the related end for this relationship group, we iterate over the relationship entries in the group and make sure that the key of the related object is added to that relationship.
The final tricky part of this code is handling stubs. Sometimes we’ll find a relationship entry, look at the state entry for the object on the other side and determine that it’s just a stub (it has only a key not a full entity). In this case, if the IRelatedEnd for this relationship is a collection, we just skip it because carrying along stubs in collections doesn’t help anything. Usually you won’t encounter stubs for collections because the EF doesn’t automatically retrieve relationship info for collections the way it does for references—this is because the update system doesn’t need to reason about stub relationships for collections (if you want to dive into why this is, that’s a topic for a whole post of its own; for now just trust me). If it is a reference, though, then we set the EntityKey from the stub onto the EntityKey property of the EntityReference. Due to a limitation in beta 3, we have to use reflection both to determine if the IRelatedEnd is a reference rather than a collection and to set the EntityKey property on the underlying object. It’s our intention to fix that in the next release so that IRelatedEnd will directly expose whether the end is a collection or a reference and (if it’s a reference) the EntityKey of the related end so that no reflection will be needed here.
OK. Without further ado, here’s the final function:
private static void SetRelationships(ObjectContext context, IEntityWithRelationships target)
{
Debug.Assert(target is IEntityWithKey);
EntityKey targetKey = ((IEntityWithKey)target).EntityKey;
foreach (var relationshipGroup in from entry in context.ObjectStateManager
.GetObjectStateEntries(EntityState.Unchanged|
EntityState.Deleted)
where (entry.IsRelationship == true) &&
(((EntityKey)entry.OriginalValues[0] == targetKey) ||
((EntityKey)entry.OriginalValues[1] == targetKey))
group entry by ((IExtendedDataRecord)entry.OriginalValues)
.DataRecordInfo.RecordType.EdmType)
{
AssociationType associationType = (AssociationType)relationshipGroup.Key;
IRelatedEnd relatedEnd = target.RelationshipManager.GetRelatedEnd(associationType.Name,
otherEndRole(relationshipGroup.First(), targetKey));
foreach (ObjectStateEntry relationshipEntry in relationshipGroup)
{
ObjectStateEntry otherEndEntry = context.ObjectStateManager
.GetObjectStateEntry(otherEndKey(relationshipEntry, targetKey));
if (!relatedEnd.Contains(otherEndEntry.EntityKey))
{
if (otherEndEntry.Entity == null)
{
Type relatedType = relatedEnd.GetType();
if (relatedType.GetGenericTypeDefinition() == typeof(EntityReference<>))
{
PropertyInfo pi = relatedType.GetProperty("EntityKey");
pi.SetValue(relatedEnd, otherEndEntry.EntityKey, null);
}
}
else
{
IEntityWithRelationships otherEnd = (IEntityWithRelationships)context
.CreateOriginalValuesObject(otherEndEntry.Entity);
relatedEnd.Add(otherEnd);
SetRelationships(context, otherEnd);
}
}
}
}
}
So, if you put all of these functions into somewhere convenient, you can use it something like this:
Room room = db.Rooms.First();
room.Exits.Load();
// modify one object's property
room.Lighting += 10;
// muck with the graph
room.Exits.Remove(room.Exits.First());
room.Exits.Remove(room.Exits.First());
Exit newExit = new Exit();
newExit.Name = "*** NEW EXIT ***";
room.Exits.Add(newExit);
// retrieve the original graph – this should match what was originally retrieved from the DB
Room originalGraph = (Room) db.CreateOriginalValuesGraph(room);
That’s all there is to it! At the moment, this is mostly just an exercise which I hope will be helpful for learning about object services, but I think it likely that these routines will come in handy as we do some experiments around disconnected operation (web services, asp.net, etc.).
- Danny
Comments
Anonymous
January 17, 2008
In this previous post and its follow-on I shared a series of extension methods that I’ve been using toAnonymous
September 03, 2008
hi Danny, Your code is very useful for me but the method SetRelationships sometime cause stack overflow. How can I fix it ? thanksAnonymous
September 07, 2008
Sorry for not responding sooner. Sounds like there's an issue with the recursion, but I'm not sure just what it is. If you can produce something that will repro the problem regularly and share it with me, that would be a huge help. If not, I'll try to get the time to look at this generally and see what I can find out, but I'm not sure exactly when that will happen.
- Danny
Anonymous
September 22, 2008
thanks for your good intention, the idea behind the method CreateOriginalValuesGraph is very good, but in practice we do not always want to get an entity with full graph. This mean, in some cases, we need to get entity's original value that including some relations, not all of relations (I've just imagine it like ObjectQuery.Include() method). So can you suggest idea to implement the method like this ? DatAnonymous
September 22, 2008
The truth is that I just don't have the time to sit down and write this at the moment, but I don't think it would be that hard. You would just need to consider whether or not a relationship type is one you want to recurse into or skip before making the recursive call at the bottom of SetRelationships. The complexity would mostly come in the form of parsing the include statements and maintaining a stack showing what navigations you have done in order to compare with the include.
- Danny