EF: Attach a graph

In a lot of cases (WCF scenario for example) we need to attach a graph to a context. But the graph can be modified when it is detached. So you have to apply modifications when you attach it back. To do this, ObjectContext class has a method ApplyPropertyChanges. This method only applies to the scalar properties, so it’s insufficient for graphs.

Moreover, there is a problem with attachment. When you want to attach a graph, if some entities are new (EntityKey equals null), you will have an exception. So you must break the graph and add or attach the entity in accordance to whether they are new or not.

In a graph, you can:

  • Modify an entity
  • Add an entity and link it to the graph
  • Add a relation between two entities of the graph
  • Delete a relation between two entities of the graph
  • Delete an entity from the graph

If you work outside of the context, I think it’s a pity to report the removal of an entity to a context. Why?

In fact, in a WCF scenario for example, you never get the same ObjectContext. So to delete an entity, it means that you need to load it from DB to delete it then. In this case, I prefer to use a SSDL Function (which can be a DB stored procedure) to delete it directly with just the entity key.

For many to many relationship, there is a lot of chance that if you delete the association between the two entities, you will lose one entity on the graph. As my AttachGraph method doesn’t take the original graph as parameter, the method can’t determine if the graph loses this entity (in comparison to the DB) because the association was deleted or because the entity wasn’t in the original graph. So my method doesn’t support this scenario. For zero or one to many relationships, my method reports it only if the entity which has the EntityReference property stayed in the graph. It supports all other modification cases.

public static class AttachExtension

{

    public static void AttachGraph(this EntityObject entity, ObjectContext context, Func<ObjectContext> createContext)

    {

        if (entity == null)

            return;

        bool b;

        entity.AttachGraph(context, createContext, out b);

    }

    public static void AttachGraph(this EntityObject entity, ObjectContext context, Func<ObjectContext> createContext, out bool isNewEntity)

    {

        IEnumerable<string> changeProperties;

        isNewEntity = entity.IsNewEntity(createContext, out changeProperties);

        if (entity.EntityState != EntityState.Detached)

            (entity as IEntityWithChangeTracker).SetChangeTracker(null);

        AttachOrAddGraph(context, entity, isNewEntity, changeProperties, createContext);

    }

 

    private static bool IsNewEntity(this EntityObject entity, Func<object, bool> entityMap)

    {

        return entityMap(entity);

    }

    private static bool IsNewEntity(this EntityObject entity, Func<ObjectContext> createContext, out IEnumerable<string> changedProperties)

    {

        IEnumerable<string> changedPropertiesValue = new string[0];

        var value = IsNewEntity(entity, e =>

        {

            var entityWithKey = (IEntityWithKey)e;

            if (entityWithKey.EntityKey == null || entityWithKey.EntityKey.EntityKeyValues == null)

                return true;

            using (var context2 = createContext())

            {

                object outEntity;

                if (context2.TryGetObjectByKey(entityWithKey.EntityKey, out outEntity))

                {

                    var entity2 = outEntity as EntityObject;

                    context2.ApplyPropertyChanges(entity2.EntityKey.EntitySetName, entity);

                    changedPropertiesValue = context2.ObjectStateManager.GetObjectStateEntry(entity2).GetModifiedProperties();

                    return false;

                }

                return true;

            }

        });

        changedProperties = changedPropertiesValue;

        return value;

    }

 

    private static void AttachOrAddGraph(ObjectContext context, EntityObject entity, bool isNewEntity, IEnumerable<string> changedProperties, Func<ObjectContext> createContext)

    {

        var relatedEntities = entity.RelatedEntities().Where(e => e.Value.Length > 0).ToList();

        var relatedEntityKeys = new Dictionary<IRelatedEnd, EntityKey>();

        foreach (var relatedEndInfo in relatedEntities)

        {

            EntityReference entityReference;

            if ((entityReference = relatedEndInfo.Key as EntityReference) != null)

                relatedEntityKeys.Add(entityReference, entityReference.EntityKey);

            foreach (EntityObject relatedEntity in relatedEndInfo.Value)

                relatedEndInfo.Key.Remove(relatedEntity);

        }

 

        EntityObject entityObject = null;

        if (isNewEntity)

            context.AddObject(entity.EntitySetName(context), entity);

        else

        {

            entityObject = context.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Modified | EntityState.Unchanged).Select(ose => ose.Entity).OfType<EntityObject>().Where(e => e.EntityKey == entity.EntityKey).FirstOrDefault();

            if (entityObject == null)

            {

                context.Attach(entity);

                var changedPropertiesEnumerator = changedProperties.GetEnumerator();

                if (changedPropertiesEnumerator.MoveNext())

                {

                    var ose = context.ObjectStateManager.GetObjectStateEntry(entity);

                    ose.SetModified();

                    do

                    {

                        ose.SetModifiedProperty(changedPropertiesEnumerator.Current);

                    } while (changedPropertiesEnumerator.MoveNext());

                }

            }

        }

 

        foreach (var relatedEndInfo in relatedEntities)

        {

            if (relatedEndInfo.Value.Any())

                foreach (EntityObject relatedEntity in relatedEndInfo.Value)

                {

                    bool relatedEntityIsNew;

                    relatedEntity.AttachGraph(context, createContext, out relatedEntityIsNew);

                    if (isNewEntity)

                        relatedEndInfo.Key.Add(relatedEntity);

                    else if (relatedEntityIsNew)

                    {

                        EntityReference entityReference;

                        if ((entityReference = relatedEndInfo.Key as EntityReference) != null)

                            using (var context2 = createContext())

                            {

                                AttachOldEntityReference(entity, context, entityReference, (EntityReference)((EntityObject)context2.GetObjectByKey(entity.EntityKey)).RelatedEnds().Where(re => re.RelationshipName == relatedEndInfo.Key.RelationshipName).First());

                            }

                        relatedEndInfo.Key.Add(relatedEntity);

                    }

                    else

                    {

                        using (var context2 = createContext())

                        {

                            var entity2 = (EntityObject)context2.GetObjectByKey(entity.EntityKey);

                            var relatedEnd2 = entity2.RelatedEnds().Where(re => re.RelationshipName == relatedEndInfo.Key.RelationshipName).First();

                            var association = context2.MetadataWorkspace.GetItemCollection(DataSpace.CSpace).OfType<AssociationType>().Where(at => at.FullName == relatedEndInfo.Key.RelationshipName).First();

                            bool relationExistsInDB;

                            if (association.AssociationEndMembers[0].RelationshipMultiplicity == RelationshipMultiplicity.Many && association.AssociationEndMembers[1].RelationshipMultiplicity == RelationshipMultiplicity.Many)

                            {

                                var entityCondition = GenerateESQLCondition(entity, “entity”);

                                var relatedCondition = GenerateESQLCondition(relatedEntity, “relatedEntity”);

                                var query = context2.CreateQuery<DbDataRecord>(string.Format(“SELECT EXISTS(SELECT 1 FROM entity.{0} AS relatedEntity WHERE {1}) AS RelationExistInDB FROM {2}.{3} AS entity WHERE {4}”, context2.MetadataWorkspace.GetItemCollection(DataSpace.CSpace).OfType<EntityType>().Where(at => at.Name == entity.GetType().Name).First().NavigationProperties.First(np => ((AssociationType)np.MetadataProperties.First(mp => mp.Name == “RelationshipType”).Value).FullName ==

                                relatedEnd2.RelationshipName).Name, relatedCondition.Key, context2.DefaultContainerName, entity2.EntityKey.EntitySetName, entityCondition.Key), (entityCondition.Value.Union(relatedCondition.Value)).ToArray());

                                relationExistsInDB = query.First().GetBoolean(0);

                            }

                            else

                            {

                                EntityReference entityReference;

                                if ((entityReference = relatedEndInfo.Key as EntityReference) != null)

                                    AttachOldEntityReference(entity, context, entityReference, (EntityReference)relatedEnd2);

 

                                context2.GetObjectByKey(relatedEntity.EntityKey);

                                relationExistsInDB = relatedEnd2.GetContents().Any(rei => rei.EntityKey == relatedEntity.EntityKey);

                            }

 

                            if (relationExistsInDB)

                            {

                                if (entityObject == null)

                                    relatedEndInfo.Key.Attach(relatedEntity);

                                else

                                    entityObject.RelatedEnds().Where(re => re.RelationshipName == relatedEndInfo.Key.RelationshipName).First().Attach(relatedEntity);

                            }

                            else

                                relatedEndInfo.Key.Add(relatedEntity);

                        }

                    }

                }

            else

            {

                EntityReference entityReference;

                if (!isNewEntity && (entityReference = relatedEndInfo.Key as EntityReference) != null && entityReference.EntityKey == null)

                {

                    using (var context2 = createContext())

                    {

                        var entity2 = (EntityObject)context2.GetObjectByKey(entity.EntityKey);

                        var relatedEnd2 = (EntityReference)entity2.RelatedEnds().Where(re => re.RelationshipName == entityReference.RelationshipName).First();

                        AttachOldEntityReference(entity, context, entityReference, relatedEnd2);

                    }

                    EntityKey outReferenceEntityKey;

                    if (relatedEntityKeys.TryGetValue(relatedEndInfo.Key, out outReferenceEntityKey))

                        entityReference.EntityKey = outReferenceEntityKey;

                    else

                        entityReference.EntityKey = null;

                }

            }

        }

    }

 

    private static void AttachOldEntityReference(EntityObject entity, ObjectContext context, EntityReference entityReference, EntityReference entityReference2)

    {

        if (entityReference.EntityKey != entityReference2.EntityKey)

        {

            int associationAdded = context.ObjectStateManager.GetObjectStateEntries(EntityState.Added).Where(ose => ose.EntitySet is AssociationSet).Count();

            entityReference.EntityKey = entityReference2.EntityKey;

            var q = context.ObjectStateManager.GetObjectStateEntries(EntityState.Added).Where(ose => ose.EntitySet is AssociationSet);

            if (q.Count() != associationAdded)

                q.Last().AcceptChanges();

        }

    }

 

    private static KeyValuePair<string, List<ObjectParameter>> GenerateESQLCondition(EntityObject entity, string parameterName)

    {

        string value = null;

        var whereParameters = new List<ObjectParameter>();

        foreach (var entityKey in entity.EntityKey.EntityKeyValues)

        {

            var whereParameterName = string.Concat(parameterName, entityKey.Key);

            var condition = string.Format(“{0}.{1} = @{2}”, parameterName, entityKey.Key, whereParameterName);

            whereParameters.Add(new ObjectParameter(whereParameterName, entityKey.Value));

            if (value == null)

                value = condition;

            else

                value = string.Concat(” AND “, condition);

        }

        return new KeyValuePair<string, List<ObjectParameter>>(value, whereParameters);

    }

 

    private static IEnumerable<KeyValuePair<IRelatedEnd, EntityObject[]>> RelatedEntities(this EntityObject entity)

    {

        foreach (var relatedEnd in entity.RelatedEnds())

            yield return new KeyValuePair<IRelatedEnd, EntityObject[]>(relatedEnd, relatedEnd.GetContents().ToArray());

    }

 

    private static IEnumerable<IRelatedEnd> RelatedEnds(this EntityObject entity)

    {

        foreach (var relatedEnd in ((IEntityWithRelationships)entity).RelationshipManager.GetAllRelatedEnds())

            yield return relatedEnd;

    }

 

    private static IEnumerable<EntityObject> GetContents(this IRelatedEnd relatedEnd)

    {

        foreach (object entity in relatedEnd)

            yield return entity as EntityObject;

    }

 

    private static string EntitySetName(this EntityObject entity, ObjectContext context)

    {

        if (entity.EntityKey != null)

            return entity.EntityKey.EntitySetName;

        return context.MetadataWorkspace.GetItemCollection(DataSpace.CSpace).OfType<EntityContainer>().First().BaseEntitySets.First(es =>

        {

            var entityTypeMetadata = context.MetadataWorkspace.GetItemCollection(DataSpace.CSpace).OfType<EdmType>().First(ese => ese.Name == entity.GetType().Name);

            while (entityTypeMetadata.BaseType != null)

                entityTypeMetadata = entityTypeMetadata.BaseType;

            return es.ElementType == entityTypeMetadata;

        }).Name;

    }

}


I want to do a big big big thanks to Jeff for all his advices.

This entry was posted in 7671, 7674. Bookmark the permalink.

7 Responses to EF: Attach a graph

  1. Ryan B says:

    This is great, thanks.

  2. Matthieu MEZIL says:

    You’re welcome Smile

  3. devMomentum says:

    There is an interesting post on that subject here (with an answer from Daniel Simmons):
    http://social.msdn.microsoft.com/forums/en-US/adodotnetentityframework/thread/d1447559-4620-4094-8761-a98935348499/

  4. Rick Blyth says:

    Hi Matthieu,

    Firstly, thanks for the contribution, it almost works perfectly for me however I’m getting an error when I try to save a graph that has multiple references to the same object.

    I get the error “An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.” and it occurs on the second line below:

    if (isNewEntity) relatedEndInfo.Key.Add(relatedEntity);

    in the AttachOrAddGraph method.

    It isn’t a new entity so I don’t see why it goes in there. Any help would be greatly appreciated!

  5. Matthieu MEZIL says:

    Hi Rick.
    I am surprised, I tried to realise it with no problem.
    Can you send me your DB and your code at matthieu.mezil@live.fr please?
    Thanks
    Matthieu

  6. Fedosov says:

    I corrected the first line of the AttachOrAddGraph method:
    var relatedEntities = entity.RelatedEntities().Where(e => e.Value.Length > 0).ToList();

    and all work correctly

  7. Matthieu MEZIL says:

    You’re right. It happens when you defined only one navigation property for an association.
    I will fix the code in the post.
    Thanks.

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>