// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.

namespace System.Data.Entity.Core.Objects.DataClasses
{
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data.Entity.Core.Common.Utils;
    using System.Data.Entity.Core.Mapping;
    using System.Data.Entity.Core.Metadata.Edm;
    using System.Data.Entity.Core.Objects.Internal;
    using System.Data.Entity.Resources;
    using System.Data.Entity.Utilities;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.Linq;
    using System.Runtime.Serialization;

    /// <summary>
    /// Container for the lazily created relationship navigation
    /// property objects (collections and refs).
    /// </summary>
    [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")]
    [Serializable]
    public class RelationshipManager
    {
        // ------------
        // Constructors
        // ------------

        // This method is private in order to force all creation of this
        // object to occur through the public static Create method.
        // See comments on that method for more details.
        private RelationshipManager()
        {
            _entityWrapperFactory = new EntityWrapperFactory();
            _expensiveLoader = new ExpensiveOSpaceLoader();
        }

        // <summary>
        // For testing.
        // </summary>
        internal RelationshipManager(ExpensiveOSpaceLoader expensiveLoader)
        {
            _entityWrapperFactory = new EntityWrapperFactory();
            _expensiveLoader = expensiveLoader ?? new ExpensiveOSpaceLoader();
        }

        // ------
        // Fields
        // ------

        // The following fields are serialized.  Adding or removing a serialized field is considered
        // a breaking change.  This includes changing the field type or field name of existing
        // serialized fields. If you need to make this kind of change, it may be possible, but it
        // will require some custom serialization/deserialization code.

        // Note that this field should no longer be used directly.  Instead, use the _wrappedOwner
        // field.  This field is retained only for compatibility with the serialization format introduced in v1.
        private IEntityWithRelationships _owner;

        private List<RelatedEnd> _relationships;

        [NonSerialized]
        private bool _nodeVisited;

        [NonSerialized]
        private IEntityWrapper _wrappedOwner;

        [NonSerialized]
        private EntityWrapperFactory _entityWrapperFactory;

        [NonSerialized]
        private ExpensiveOSpaceLoader _expensiveLoader;

        // ----------
        // Properties
        // ----------

        // <summary>
        // For testing.
        // </summary>
        internal void SetExpensiveLoader(ExpensiveOSpaceLoader loader)
        {
            DebugCheck.NotNull(loader);

            _expensiveLoader = loader;
        }

        // <summary>
        // Returns a defensive copy of all the known relationships.  The copy is defensive because
        // new items may get added to the collection while the caller is iterating over it.  Without
        // the copy this would cause an exception for concurrently modifying the collection.
        // </summary>
        internal IEnumerable<RelatedEnd> Relationships
        {
            get
            {
                EnsureRelationshipsInitialized();
                return _relationships.ToArray();
            }
        }

        // <summary>
        // Lazy initialization of the _relationships collection.
        // </summary>
        private void EnsureRelationshipsInitialized()
        {
            if (null == _relationships)
            {
                _relationships = new List<RelatedEnd>();
            }
        }

        // <summary>
        // this flag is used to keep track of nodes which have
        // been visited. Currently used for Exclude operation.
        // </summary>
        internal bool NodeVisited
        {
            get { return _nodeVisited; }
            set { _nodeVisited = value; }
        }

        // <summary>
        // Provides access to the entity that owns this manager in its wrapped form.
        // </summary>
        internal IEntityWrapper WrappedOwner
        {
            get
            {
                if (_wrappedOwner == null)
                {
                    _wrappedOwner = EntityWrapperFactory.CreateNewWrapper(_owner, null);
                }
                return _wrappedOwner;
            }
        }

        internal virtual EntityWrapperFactory EntityWrapperFactory
        {
            get { return _entityWrapperFactory; }
        }

        // -------
        // Methods
        // -------

        /// <summary>
        /// Creates a new <see cref="T:System.Data.Entity.Core.Objects.DataClasses.RelationshipManager" /> object.
        /// </summary>
        /// <remarks>
        /// Used by data classes that support relationships. If the change tracker
        /// requests the RelationshipManager property and the data class does not
        /// already have a reference to one of these objects, it calls this method
        /// to create one, then saves a reference to that object. On subsequent accesses
        /// to that property, the data class should return the saved reference.
        /// The reason for using a factory method instead of a public constructor is to
        /// emphasize that this is not something you would normally call outside of a data class.
        /// By requiring that these objects are created via this method, developers should
        /// give more thought to the operation, and will generally only use it when
        /// they explicitly need to get an object of this type. It helps define the intended usage.
        /// </remarks>
        /// <returns>
        /// The requested <see cref="T:System.Data.Entity.Core.Objects.DataClasses.RelationshipManager" />.
        /// </returns>
        /// <param name="owner">Reference to the entity that is calling this method.</param>
        public static RelationshipManager Create(IEntityWithRelationships owner)
        {
            Check.NotNull(owner, "owner");
            var rm = new RelationshipManager();
            rm._owner = owner;
            return rm;
        }

        // <summary>
        // Factory method that creates a new, uninitialized RelationshipManager.  This should only be
        // used to create a RelationshipManager for an IEntityWrapper for an entity that does not
        // implement IEntityWithRelationships.  For entities that implement IEntityWithRelationships,
        // the Create(IEntityWithRelationships) method should be used instead.
        // </summary>
        // <returns> The new RelationshipManager </returns>
        internal static RelationshipManager Create()
        {
            return new RelationshipManager();
        }

        // <summary>
        // Replaces the existing wrapped owner with one that potentially contains more information,
        // such as an entity key.  Both must wrap the same entity.
        // </summary>
        internal void SetWrappedOwner(IEntityWrapper wrappedOwner, object expectedOwner)
        {
            _wrappedOwner = wrappedOwner;
            Debug.Assert(
                _owner != null || !(wrappedOwner.Entity is IEntityWithRelationships),
                "_owner should only be null if entity is not IEntityWithRelationships");
            // We need to check that the RelationshipManager created by the entity has the correct owner set,
            // since the entity can pass any value into RelationshipManager.Create().
            if (_owner != null
                && !ReferenceEquals(expectedOwner, _owner))
            {
                throw new InvalidOperationException(Strings.RelationshipManager_InvalidRelationshipManagerOwner);
            }

            if (null != _relationships)
            {
                // Not using defensive copy here since SetWrappedOwner should not cause change in underlying
                // _relationships collection.
                foreach (var relatedEnd in _relationships)
                {
                    relatedEnd.SetWrappedOwner(wrappedOwner);
                }
            }
        }

        internal EntityCollection<TTargetEntity> GetRelatedCollection<TSourceEntity, TTargetEntity>(
            AssociationEndMember sourceMember, AssociationEndMember targetMember, NavigationPropertyAccessor sourceAccessor,
            NavigationPropertyAccessor targetAccessor, RelatedEnd existingRelatedEnd)
            where TSourceEntity : class
            where TTargetEntity : class
        {
            var relationshipName = sourceMember.DeclaringType.FullName;
            var targetRoleName = targetMember.Name;
            var sourceRoleMultiplicity = sourceMember.RelationshipMultiplicity;

            RelatedEnd relatedEnd;
            TryGetCachedRelatedEnd(relationshipName, targetRoleName, out relatedEnd);

            var previousCollection = relatedEnd as EntityCollection<TTargetEntity>;
            if (existingRelatedEnd == null)
            {
                if (relatedEnd != null)
                {
                    // Because this is a private method that will only be called for target roles that actually have a
                    // multiplicity that works with EntityReference, this should never be null. If the user requests
                    // a collection or reference and it doesn't match the target role multiplicity, it will be detected
                    // in the public GetRelatedCollection<T> or GetRelatedReference<T>
                    Debug.Assert(previousCollection != null, "should never receive anything but an EntityCollection here");
                    return previousCollection;
                }
                else
                {
                    var navigation = new RelationshipNavigation(
                        (AssociationType) sourceMember.DeclaringType, sourceMember.Name, targetMember.Name, 
                        sourceAccessor, targetAccessor);
                    return
                        CreateRelatedEnd<TSourceEntity, TTargetEntity>(
                            navigation, sourceRoleMultiplicity, RelationshipMultiplicity.Many, existingRelatedEnd) as
                        EntityCollection<TTargetEntity>;
                }
            }
            else
            {
                // There is no need to suppress events on the existingRelatedEnd because setting events on a disconnected
                // EntityCollection is an InvalidOperation
                Debug.Assert(existingRelatedEnd._onAssociationChanged == null, "Disconnected RelatedEnd had events");

                if (relatedEnd != null)
                {
                    Debug.Assert(_relationships != null, "Expected _relationships to be non-null.");
                    _relationships.Remove(relatedEnd);
                }

                var navigation = new RelationshipNavigation(
                    (AssociationType)sourceMember.DeclaringType, sourceMember.Name, targetMember.Name,
                    sourceAccessor, targetAccessor);
                var collection =
                    CreateRelatedEnd<TSourceEntity, TTargetEntity>(
                        navigation, sourceRoleMultiplicity, RelationshipMultiplicity.Many, existingRelatedEnd) as
                    EntityCollection<TTargetEntity>;

                if (collection != null)
                {
                    var doCleanup = true;
                    try
                    {
                        RemergeCollections(previousCollection, collection);
                        doCleanup = false;
                    }
                    finally
                    {
                        // An error occured so we need to put the previous relatedEnd back into the RelationshipManager
                        if (doCleanup && relatedEnd != null)
                        {
                            Debug.Assert(_relationships != null, "Expected _relationships to be non-null.");
                            _relationships.Remove(collection);
                            _relationships.Add(relatedEnd);
                        }
                    }
                }
                return collection;
            }
        }

        // <summary>
        // Re-merge items from collection so that relationship fixup is performed.
        // Ensure that any items in previous collection are excluded from the re-merge
        // </summary>
        // <param name="previousCollection"> The previous EntityCollection containing items that have already had fixup performed </param>
        // <param name="collection"> The new EntityCollection </param>
        private static void RemergeCollections<TTargetEntity>(
            EntityCollection<TTargetEntity> previousCollection,
            EntityCollection<TTargetEntity> collection)
            where TTargetEntity : class
        {
            DebugCheck.NotNull(collection);
            // If there is a previousCollection, we only need to merge the items that are 
            // in the collection but not in the previousCollection
            // Ensure that all of the items in the previousCollection are already in the new collection

            var relatedEntityCount = 0;

            // We will be modifing the collection's enumerator, so we need to make a copy of it
            var tempEntities = new List<IEntityWrapper>(collection.CountInternal);
            foreach (var wrappedEntity in collection.GetWrappedEntities())
            {
                tempEntities.Add(wrappedEntity);
            }

            // Iterate through the entities that require merging
            // If the previousCollection already contained the entity, no additional work is needed
            // If the previousCollection did not contain the entity,
            //   then remove it from the collection and re-add it to force relationship fixup
            foreach (var wrappedEntity in tempEntities)
            {
                var requiresMerge = true;
                if (previousCollection != null)
                {
                    // There is no need to merge and do fixup if the entity was already in the previousCollection because
                    // fixup would have already taken place when it was added to the previousCollection
                    if (previousCollection.ContainsEntity(wrappedEntity))
                    {
                        relatedEntityCount++;
                        requiresMerge = false;
                    }
                }

                if (requiresMerge)
                {
                    // Remove and re-add the item to the collections to force fixup
                    collection.Remove(wrappedEntity, false);
                    collection.Add(wrappedEntity);
                }
            }

            // Ensure that all of the items in the previousCollection are already in the new collection
            if (previousCollection != null
                && relatedEntityCount != previousCollection.CountInternal)
            {
                throw new InvalidOperationException(Strings.Collections_UnableToMergeCollections);
            }
        }

        internal EntityReference<TTargetEntity> GetRelatedReference<TSourceEntity, TTargetEntity>(
            AssociationEndMember sourceMember, AssociationEndMember targetMember, NavigationPropertyAccessor sourceAccessor,
            NavigationPropertyAccessor targetAccessor, RelatedEnd existingRelatedEnd)
            where TSourceEntity : class
            where TTargetEntity : class
        {
            var relationshipName = sourceMember.DeclaringType.FullName;
            var targetRoleName = targetMember.Name;
            var sourceRoleMultiplicity = sourceMember.RelationshipMultiplicity;

            EntityReference<TTargetEntity> entityRef;
            RelatedEnd relatedEnd;

            if (TryGetCachedRelatedEnd(relationshipName, targetRoleName, out relatedEnd))
            {
                entityRef = relatedEnd as EntityReference<TTargetEntity>;
                // Because this is a private method that will only be called for target roles that actually have a
                // multiplicity that works with EntityReference, this should never be null. If the user requests
                // a collection or reference and it doesn't match the target role multiplicity, it will be detected
                // in the public GetRelatedCollection<T> or GetRelatedReference<T>
                Debug.Assert(entityRef != null, "should never receive anything but an EntityReference here");
                return entityRef;
            }
            else
            {
                var navigation = new RelationshipNavigation(
                    (AssociationType)sourceMember.DeclaringType, sourceMember.Name, targetMember.Name,
                    sourceAccessor, targetAccessor);
                return
                    CreateRelatedEnd<TSourceEntity, TTargetEntity>(
                        navigation, sourceRoleMultiplicity, RelationshipMultiplicity.One, existingRelatedEnd) as
                    EntityReference<TTargetEntity>;
            }
        }

        // <summary>
        // Internal version of GetRelatedEnd that works with the o-space navigation property
        // name rather than the c-space relationship name and end name.
        // </summary>
        // <param name="navigationProperty"> the name of the property to lookup </param>
        // <returns> the related end for the given property </returns>
        [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
        internal RelatedEnd GetRelatedEnd(string navigationProperty, bool throwArgumentException = false)
        {
            var wrappedOwner = WrappedOwner;
            Debug.Assert(wrappedOwner.Entity != null, "Entity is null");
            Debug.Assert(wrappedOwner.Context != null, "Context is null");
            Debug.Assert(wrappedOwner.Context.MetadataWorkspace != null, "MetadataWorkspace is null");
            Debug.Assert(wrappedOwner.Context.Perspective != null, "Perspective is null");

            var entityType = wrappedOwner.Context.MetadataWorkspace.GetItem<EntityType>(
                wrappedOwner.IdentityType.FullNameWithNesting(), DataSpace.OSpace);
            EdmMember member;
            if (!wrappedOwner.Context.Perspective.TryGetMember(entityType, navigationProperty, false, out member)
                ||
                !(member is NavigationProperty))
            {
                var message = Strings.RelationshipManager_NavigationPropertyNotFound(navigationProperty);
                throw throwArgumentException ? new ArgumentException(message) : (Exception)new InvalidOperationException(message);
            }
            var navProp = (NavigationProperty)member;
            return GetRelatedEndInternal(navProp.RelationshipType.FullName, navProp.ToEndMember.Name);
        }

        /// <summary>
        /// Returns either an <see cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityCollection`1" /> or
        /// <see
        ///     cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityReference`1" />
        /// of the correct type for the specified target role in a relationship.
        /// </summary>
        /// <returns>
        /// <see cref="T:System.Data.Entity.Core.Objects.DataClasses.IRelatedEnd" /> representing the
        /// <see
        ///     cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityCollection`1" />
        /// or
        /// <see
        ///     cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityReference`1" />
        /// that was retrieved.
        /// </returns>
        /// <param name="relationshipName">Name of the relationship in which  targetRoleName  is defined. The relationship name is not namespace qualified.</param>
        /// <param name="targetRoleName">Target role to use to retrieve the other end of  relationshipName .</param>
        /// <exception cref="T:System.ArgumentNullException"> relationshipName  or  targetRoleName  is null.</exception>
        /// <exception cref="T:System.InvalidOperationException">The source type does not match the type of the owner.</exception>
        /// <exception cref="T:System.ArgumentException"> targetRoleName  is invalid or unable to find the relationship type in the metadata.</exception>
        public IRelatedEnd GetRelatedEnd(string relationshipName, string targetRoleName)
        {
            return GetRelatedEndInternal(PrependNamespaceToRelationshipName(relationshipName), targetRoleName);
        }

        // Internal version of GetRelatedEnd which returns the RelatedEnd as a RelatedEnd rather than an IRelatedEnd
        internal RelatedEnd GetRelatedEndInternal(string relationshipName, string targetRoleName)
        {
            DebugCheck.NotNull(relationshipName);
            DebugCheck.NotNull(targetRoleName);

            var wrappedOwner = WrappedOwner;
            if (wrappedOwner.Context == null
                && wrappedOwner.RequiresRelationshipChangeTracking)
            {
                throw new InvalidOperationException(Strings.RelationshipManager_CannotGetRelatEndForDetachedPocoEntity);
            }

            var associationType = GetRelationshipType(relationshipName);
            Debug.Assert(associationType != null);

            return GetRelatedEndInternal(relationshipName, targetRoleName, /*existingRelatedEnd*/ null, associationType);
        }

        private RelatedEnd GetRelatedEndInternal(
            string relationshipName, string targetRoleName, RelatedEnd existingRelatedEnd, AssociationType relationship)
        {
            DebugCheck.NotNull(relationshipName);
            DebugCheck.NotNull(targetRoleName);
            // existingRelatedEnd can be null if we are not trying to initialize an existing end
            DebugCheck.NotNull(relationship);

            AssociationEndMember sourceEnd;
            AssociationEndMember targetEnd;
            Debug.Assert(relationship.AssociationEndMembers.Count == 2, "Only 2-way relationships are currently supported");
            GetAssociationEnds(relationship, targetRoleName, out sourceEnd, out targetEnd);

            // Validate that the source type matches the type of the owner
            var sourceEntityType = MetadataHelper.GetEntityTypeForEnd(sourceEnd);
            Debug.Assert(
                sourceEntityType.DataSpace == DataSpace.OSpace && sourceEntityType.ClrType != null,
                "sourceEntityType must contain an ospace type");
            var sourceType = sourceEntityType.ClrType;
            var wrappedOwner = WrappedOwner;
            if (!(sourceType.IsAssignableFrom(wrappedOwner.IdentityType)))
            {
                throw new InvalidOperationException(
                    Strings.RelationshipManager_OwnerIsNotSourceType(
                        wrappedOwner.IdentityType.FullName, sourceType.FullName, sourceEnd.Name, relationshipName));
            }
            
            return VerifyRelationship(relationship, sourceEnd.Name)
                // Call a dynamic method that will call either GetRelatedCollection<T, T> or GetRelatedReference<T, T> for this relationship
                ? DelegateFactory.GetRelatedEnd(this, sourceEnd, targetEnd, existingRelatedEnd) 
                : null;
        }

        internal RelatedEnd GetRelatedEndInternal(AssociationType csAssociationType, AssociationEndMember csTargetEnd)
        {
            var wrappedOwner = WrappedOwner;
            if (wrappedOwner.Context == null
                && wrappedOwner.RequiresRelationshipChangeTracking)
            {
                throw new InvalidOperationException(Strings.RelationshipManager_CannotGetRelatEndForDetachedPocoEntity);
            }

            var osAssociationType = GetRelationshipType(csAssociationType);
            Debug.Assert(osAssociationType != null);
            Debug.Assert(osAssociationType.DataSpace == DataSpace.OSpace);

            AssociationEndMember osSourceEnd;
            AssociationEndMember osTargetEnd;
            GetAssociationEnds(osAssociationType, csTargetEnd.Name, out osSourceEnd, out osTargetEnd);

            var sourceEntityType = MetadataHelper.GetEntityTypeForEnd(osSourceEnd);
            var sourceType = sourceEntityType.ClrType;

            if (!(sourceType.IsAssignableFrom(wrappedOwner.IdentityType)))
            {
                throw new InvalidOperationException(
                    Strings.RelationshipManager_OwnerIsNotSourceType(wrappedOwner.IdentityType.FullName,
                        sourceType.FullName, osSourceEnd.Name, csAssociationType.FullName));
            }

            return VerifyRelationship(osAssociationType, csAssociationType, osSourceEnd.Name)
                ? DelegateFactory.GetRelatedEnd(this, osSourceEnd, osTargetEnd, null)
                : null;
        }

        private static void GetAssociationEnds(AssociationType associationType, string targetRoleName, 
            out AssociationEndMember sourceEnd, out AssociationEndMember targetEnd)
        {
            targetEnd = associationType.TargetEnd;

            if (targetEnd.Identity != targetRoleName)
            {
                sourceEnd = targetEnd;
                targetEnd = associationType.SourceEnd;

                if (targetEnd.Identity != targetRoleName)
                {
                    throw new InvalidOperationException(
                        Strings.RelationshipManager_InvalidTargetRole(associationType.FullName, targetRoleName));
                }
            }
            else
            {
                sourceEnd = associationType.SourceEnd;
            }            
        }

        /// <summary>
        /// Takes an existing EntityReference that was created with the default constructor and initializes it using the provided relationship and target role names.
        /// This method is designed to be used during deserialization only, and will throw an exception if the provided EntityReference has already been initialized,
        /// if the relationship manager already contains a relationship with this name and target role, or if the relationship manager is already attached to a ObjectContext.W
        /// </summary>
        /// <param name="relationshipName">The relationship name.</param>
        /// <param name="targetRoleName">The role name of the related end.</param>
        /// <param name="entityReference">
        /// The <see cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityReference`1" /> to initialize.
        /// </param>
        /// <typeparam name="TTargetEntity">
        /// The type of the <see cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityReference`1" /> being initialized.
        /// </typeparam>
        /// <exception cref="T:System.InvalidOperationException">
        /// When the provided <see cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityReference`1" /> 
        /// is already initialized.-or-When the relationship manager is already attached to an
        /// <see cref="T:System.Data.Entity.Core.Objects.ObjectContext" />
        /// or when the relationship manager already contains a relationship with this name and target role.
        /// </exception>
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public void InitializeRelatedReference<TTargetEntity>(
            string relationshipName, string targetRoleName, EntityReference<TTargetEntity> entityReference)
            where TTargetEntity : class
        {
            Check.NotNull(relationshipName, "relationshipName");
            Check.NotNull(targetRoleName, "targetRoleName");
            Check.NotNull(entityReference, "entityReference");

            if (entityReference.WrappedOwner.Entity != null)
            {
                throw new InvalidOperationException(
                    Strings.RelationshipManager_ReferenceAlreadyInitialized(Strings.RelationshipManager_InitializeIsForDeserialization));
            }

            var wrappedOwner = WrappedOwner;
            if (wrappedOwner.Context != null
                && wrappedOwner.MergeOption != MergeOption.NoTracking)
            {
                throw new InvalidOperationException(
                    Strings.RelationshipManager_RelationshipManagerAttached(Strings.RelationshipManager_InitializeIsForDeserialization));
            }

            // We need the CSpace-qualified name in order to determine if this relationship already exists, so look it up.
            // If the relationship doesn't exist, we will use this type information to determine how to initialize the reference
            relationshipName = PrependNamespaceToRelationshipName(relationshipName);
            var relationship = GetRelationshipType(relationshipName);

            RelatedEnd relatedEnd;
            if (TryGetCachedRelatedEnd(relationshipName, targetRoleName, out relatedEnd))
            {
                // For some serialization scenarios, we have to allow replacing a related end that we already know about, but in those scenarios 
                // the end is always empty, so we can further restrict the user calling method method directly by doing this extra validation
                if (!relatedEnd.IsEmpty())
                {
                    entityReference.InitializeWithValue(relatedEnd);
                }
                Debug.Assert(_relationships != null, "Expected _relationships to be non-null.");
                _relationships.Remove(relatedEnd);
            }

            var reference =
                GetRelatedEndInternal(relationshipName, targetRoleName, entityReference, relationship) as EntityReference<TTargetEntity>;
            if (reference == null)
            {
                throw new InvalidOperationException(
                    Strings.EntityReference_ExpectedReferenceGotCollection(typeof(TTargetEntity).Name, targetRoleName, relationshipName));
            }
        }

        /// <summary>
        /// Takes an existing EntityCollection that was created with the default constructor and initializes it using the provided relationship and target role names.
        /// This method is designed to be used during deserialization only, and will throw an exception if the provided EntityCollection has already been initialized,
        /// or if the relationship manager is already attached to a ObjectContext.
        /// </summary>
        /// <param name="relationshipName">The relationship name.</param>
        /// <param name="targetRoleName">The target role name.</param>
        /// <param name="entityCollection">An existing EntityCollection.</param>
        /// <typeparam name="TTargetEntity"> Type of the entity represented by targetRoleName </typeparam>
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public void InitializeRelatedCollection<TTargetEntity>(
            string relationshipName, string targetRoleName, EntityCollection<TTargetEntity> entityCollection)
            where TTargetEntity : class
        {
            Check.NotNull(relationshipName, "relationshipName");
            Check.NotNull(targetRoleName, "targetRoleName");
            Check.NotNull(entityCollection, "entityCollection");

            if (entityCollection.WrappedOwner.Entity != null)
            {
                throw new InvalidOperationException(
                    Strings.RelationshipManager_CollectionAlreadyInitialized(
                        Strings.RelationshipManager_CollectionInitializeIsForDeserialization));
            }

            var wrappedOwner = WrappedOwner;
            if (wrappedOwner.Context != null
                && wrappedOwner.MergeOption != MergeOption.NoTracking)
            {
                throw new InvalidOperationException(
                    Strings.RelationshipManager_CollectionRelationshipManagerAttached(
                        Strings.RelationshipManager_CollectionInitializeIsForDeserialization));
            }

            // We need the CSpace-qualified name in order to determine if this relationship already exists, so look it up.
            // If the relationship doesn't exist, we will use this type information to determine how to initialize the reference
            relationshipName = PrependNamespaceToRelationshipName(relationshipName);
            var relationship = GetRelationshipType(relationshipName);

            var collection =
                GetRelatedEndInternal(relationshipName, targetRoleName, entityCollection, relationship) as EntityCollection<TTargetEntity>;
            if (collection == null)
            {
                throw new InvalidOperationException(
                    Strings.Collections_ExpectedCollectionGotReference(typeof(TTargetEntity).Name, targetRoleName, relationshipName));
            }
        }

        // <summary>
        // Given a relationship name that may or may not be qualified with a namespace name, this method
        // attempts to lookup a namespace using the entity type that owns this RelationshipManager as a
        // source and adds that namespace to the front of the relationship name.  If the namespace
        // can't be found, then the relationshipName is returned untouched and the expectation is that
        // other validations will fail later in the code paths that use this.
        // This method should only be used at the imediate top-level public surface since all internal
        // calls are expected to use fully qualified names already.
        // </summary>
        internal string PrependNamespaceToRelationshipName(string relationshipName)
        {
            DebugCheck.NotNull(relationshipName);

            if (!relationshipName.Contains("."))
            {
                AssociationType associationType;
                if (EntityProxyFactory.TryGetAssociationTypeFromProxyInfo(WrappedOwner, relationshipName, out associationType))
                {
                    return associationType.FullName;
                }

                if (_relationships != null)
                {
                    var fullName = _relationships
                        .Select(r => r.RelationshipName)
                        .FirstOrDefault(n => n.Substring(n.LastIndexOf('.') + 1) == relationshipName);

                    if (fullName != null)
                    {
                        return fullName;
                    }
                }

                var identityName = WrappedOwner.IdentityType.FullNameWithNesting();
                var objectItemCollection = GetObjectItemCollection(WrappedOwner);
                EdmType entityType = null;
                if (objectItemCollection != null)
                {
                    objectItemCollection.TryGetItem(identityName, out entityType);
                }
                else
                {
                    var types = _expensiveLoader.LoadTypesExpensiveWay(WrappedOwner.IdentityType.Assembly());
                    if (types != null)
                    {
                        types.TryGetValue(identityName, out entityType);
                    }
                }
                var clrEntityType = entityType as ClrEntityType;
                if (clrEntityType != null)
                {
                    var ns = clrEntityType.CSpaceNamespaceName;
                    Debug.Assert(!string.IsNullOrEmpty(ns), "Expected non-empty namespace for type.");

                    return ns + "." + relationshipName;
                }
            }
            return relationshipName;
        }

        // <summary>
        // Trys to get an ObjectItemCollection and returns null if it can;t be found.
        // </summary>
        private static ObjectItemCollection GetObjectItemCollection(IEntityWrapper wrappedOwner)
        {
            if (wrappedOwner.Context != null)
            {
                Debug.Assert(wrappedOwner.Context.MetadataWorkspace != null);

                return (ObjectItemCollection)wrappedOwner.Context.MetadataWorkspace.GetItemCollection(DataSpace.OSpace);
            }
            return null;
        }

        // <summary>
        // Trys to get the EntityType metadata and returns false if it can't be found.
        // </summary>
        private bool TryGetOwnerEntityType(out EntityType entityType)
        {
            DefaultObjectMappingItemCollection mappings;
            MappingBase map;
            if (TryGetObjectMappingItemCollection(WrappedOwner, out mappings)
                && mappings.TryGetMap(WrappedOwner.IdentityType.FullNameWithNesting(), DataSpace.OSpace, out map))
            {
                var objectMap = (ObjectTypeMapping)map;
                if (Helper.IsEntityType(objectMap.EdmType))
                {
                    entityType = (EntityType)objectMap.EdmType;
                    return true;
                }
            }

            entityType = null;
            return false;
        }

        // <summary>
        // Trys to get an DefaultObjectMappingItemCollection and returns false if it can't be found.
        // </summary>
        private static bool TryGetObjectMappingItemCollection(
            IEntityWrapper wrappedOwner, out DefaultObjectMappingItemCollection collection)
        {
            if (wrappedOwner.Context != null
                && wrappedOwner.Context.MetadataWorkspace != null)
            {
                collection = (DefaultObjectMappingItemCollection)wrappedOwner.Context.MetadataWorkspace.GetItemCollection(DataSpace.OCSpace);
                return collection != null;
            }

            collection = null;
            return false;
        }

        internal AssociationType GetRelationshipType(AssociationType csAssociationType)
        {
            var metadataWorkspace = WrappedOwner.Context.MetadataWorkspace;
            if (metadataWorkspace != null)
            {
                return metadataWorkspace.MetadataOptimization.GetOSpaceAssociationType(
                    csAssociationType, () => GetRelationshipType(csAssociationType.FullName));
            }

            return GetRelationshipType(csAssociationType.FullName);
        }

        internal AssociationType GetRelationshipType(string relationshipName)
        {
            DebugCheck.NotEmpty(relationshipName);

            AssociationType associationType = null;

            var objectItemCollection = GetObjectItemCollection(WrappedOwner);
            if (objectItemCollection != null)
            {
                associationType = objectItemCollection.GetRelationshipType(relationshipName);
            }

            if (associationType == null)
            {
                EntityProxyFactory.TryGetAssociationTypeFromProxyInfo(WrappedOwner, relationshipName, out associationType);
            }

            if (associationType == null
                && _relationships != null)
            {
                associationType = _relationships
                    .Where(e => e.RelationshipName == relationshipName)
                    .Select(e => e.RelationMetadata)
                    .OfType<AssociationType>()
                    .FirstOrDefault();
            }

            if (associationType == null)
            {
                associationType = _expensiveLoader.GetRelationshipTypeExpensiveWay(WrappedOwner.IdentityType, relationshipName);
            }

            if (associationType == null)
            {
                throw UnableToGetMetadata(WrappedOwner, relationshipName);
            }

            return associationType;
        }

        internal static Exception UnableToGetMetadata(IEntityWrapper wrappedOwner, string relationshipName)
        {
            var argException = new ArgumentException(
                Strings.RelationshipManager_UnableToFindRelationshipTypeInMetadata(relationshipName), "relationshipName");
            if (EntityProxyFactory.IsProxyType(wrappedOwner.Entity.GetType()))
            {
                return
                    new InvalidOperationException(
                        Strings.EntityProxyTypeInfo_ProxyMetadataIsUnavailable(wrappedOwner.IdentityType.FullName), argException);
            }
            else
            {
                return argException;
            }
        }

        private static IEnumerable<AssociationEndMember> GetAllTargetEnds(EntityType ownerEntityType, EntitySet ownerEntitySet)
        {
            foreach (var assocSet in ownerEntitySet.AssociationSets)
            {
                var end2EntityType = (assocSet.ElementType).AssociationEndMembers[1].GetEntityType();
                if (end2EntityType.IsAssignableFrom(ownerEntityType))
                {
                    yield return (assocSet.ElementType).AssociationEndMembers[0];
                }
                // not "else" because of associations between the same entity sets
                var end1EntityType = (assocSet.ElementType).AssociationEndMembers[0].GetEntityType();
                if (end1EntityType.IsAssignableFrom(ownerEntityType))
                {
                    yield return (assocSet.ElementType).AssociationEndMembers[1];
                }
            }
        }

        // <summary>
        // Retrieves the AssociationEndMembers that corespond to the target end of a relationship
        // given a specific CLR type that exists on the source end of a relationship
        // Note: this method can be very expensive if this RelationshipManager is not attached to an
        // ObjectContext because no OSpace Metadata is available
        // </summary>
        // <param name="entityClrType"> A CLR type that is on the source role of the relationship </param>
        // <returns> The OSpace EntityType that represents this CLR type </returns>
        private IEnumerable<AssociationEndMember> GetAllTargetEnds(Type entityClrType)
        {
            var objectItemCollection = GetObjectItemCollection(WrappedOwner);

            IEnumerable<AssociationType> associations = null;
            if (objectItemCollection != null)
            {
                // Metadata is available
                associations = objectItemCollection.GetItems<AssociationType>();
            }
            else
            {
                associations = EntityProxyFactory.TryGetAllAssociationTypesFromProxyInfo(WrappedOwner);

                if (associations == null)
                {
                    // No metadata is available, attempt to load the metadata on the fly to retrieve the AssociationTypes
                    associations = _expensiveLoader.GetAllRelationshipTypesExpensiveWay(entityClrType.Assembly());
                }
            }

            foreach (var association in associations)
            {
                // Check both ends for the presence of the source CLR type
                var referenceType = association.AssociationEndMembers[0].TypeUsage.EdmType as RefType;
                if (referenceType != null
                    && referenceType.ElementType.ClrType.IsAssignableFrom(entityClrType))
                {
                    // Return the target end
                    yield return association.AssociationEndMembers[1];
                }

                referenceType = association.AssociationEndMembers[1].TypeUsage.EdmType as RefType;
                if (referenceType != null
                    && referenceType.ElementType.ClrType.IsAssignableFrom(entityClrType))
                {
                    // Return the target end
                    yield return association.AssociationEndMembers[0];
                }
            }
        }

        private bool VerifyRelationship(AssociationType relationship, string sourceEndName)
        {
            var wrappedOwner = WrappedOwner;
            if (wrappedOwner.Context == null)
            {
                return true; // if not added to cache, can not decide- for now
            }

            var ownerKey = wrappedOwner.EntityKey;
            if (ownerKey == null)
            {
                return true; // if not added to cache, can not decide- for now
            }

            return VerifyRelationship(wrappedOwner, ownerKey, relationship, sourceEndName);
        }

        private bool VerifyRelationship(AssociationType osAssociationType, AssociationType csAssociationType, string sourceEndName)
        {
            var wrappedOwner = WrappedOwner;
            if (wrappedOwner.Context == null)
            {
                return true;
            }

            var ownerKey = wrappedOwner.EntityKey;
            if (ownerKey == null)
            {
                return true;
            }

            if (osAssociationType.Index < 0)
            {
                return VerifyRelationship(wrappedOwner, ownerKey, osAssociationType, sourceEndName);
            }

            Debug.Assert(osAssociationType.Index == csAssociationType.Index);

            var metadataWorkspace = wrappedOwner.Context.MetadataWorkspace;
            Debug.Assert(metadataWorkspace != null);

            EntitySet sourceEntitySet;
            var csAssociationSet = metadataWorkspace.MetadataOptimization.FindCSpaceAssociationSet(
                csAssociationType, sourceEndName, ownerKey.EntitySetName, ownerKey.EntityContainerName,
                out sourceEntitySet);

            if (csAssociationSet == null)
            {
                throw Error.Collections_NoRelationshipSetMatched(osAssociationType.FullName);
            }

            return true;
        }

        private static bool VerifyRelationship(IEntityWrapper wrappedOwner, EntityKey ownerKey,
            AssociationType relationship, string sourceEndName)
        {
            TypeUsage associationTypeUsage;

            // First, get the CSpace association type from the relationship name, since the helper method looks up
            // association set in the CSpace, since there is no Entity Container in the OSpace
            if (wrappedOwner.Context.Perspective.TryGetTypeByName(relationship.FullName, false /*ignoreCase*/, out associationTypeUsage))
            {
                EntitySet sourceEntitySet;
                var associationSet = wrappedOwner.Context.MetadataWorkspace.MetadataOptimization.FindCSpaceAssociationSet(
                    (AssociationType)associationTypeUsage.EdmType, sourceEndName, 
                    ownerKey.EntitySetName, ownerKey.EntityContainerName, out sourceEntitySet);

                if (associationSet == null)
                {
                    var relationshipName = relationship.FullName;
                    Debug.Assert(!String.IsNullOrEmpty(relationshipName), "empty relationshipName");
                    throw Error.Collections_NoRelationshipSetMatched(relationshipName);
                }

                Debug.Assert(
                    associationSet.AssociationSetEnds[sourceEndName].EntitySet == sourceEntitySet,
                    "AssociationSetEnd does have the matching EntitySet");
            }

            return true;
        }

        /// <summary>
        /// Gets an <see cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityCollection`1" /> of related objects with the specified relationship name and target role name.
        /// </summary>
        /// <returns>
        /// The <see cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityCollection`1" /> of related objects.
        /// </returns>
        /// <param name="relationshipName">Name of the relationship to navigate. The relationship name is not namespace qualified.</param>
        /// <param name="targetRoleName">Name of the target role for the navigation. Indicates the direction of navigation across the relationship.</param>
        /// <typeparam name="TTargetEntity">
        /// The type of the returned <see cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityCollection`1" />.
        /// </typeparam>
        /// <exception cref="T:System.InvalidOperationException">
        /// The specified role returned an <see cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityReference`1" /> instead of an
        /// <see
        ///     cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityCollection`1" />
        /// .
        /// </exception>
        public EntityCollection<TTargetEntity> GetRelatedCollection<TTargetEntity>(string relationshipName, string targetRoleName)
            where TTargetEntity : class
        {
            var collection =
                GetRelatedEndInternal(PrependNamespaceToRelationshipName(relationshipName), targetRoleName) as
                EntityCollection<TTargetEntity>;
            if (collection == null)
            {
                throw new InvalidOperationException(
                    Strings.Collections_ExpectedCollectionGotReference(typeof(TTargetEntity).Name, targetRoleName, relationshipName));
            }
            return collection;
        }

        /// <summary>
        /// Gets the <see cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityReference`1" /> for a related object by using the specified combination of relationship name and target role name.
        /// </summary>
        /// <returns>
        /// The <see cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityReference`1" /> of a related object.
        /// </returns>
        /// <param name="relationshipName">Name of the relationship to navigate. The relationship name is not namespace qualified.</param>
        /// <param name="targetRoleName">Name of the target role for the navigation. Indicates the direction of navigation across the relationship.</param>
        /// <typeparam name="TTargetEntity">
        /// The type of the returned <see cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityReference`1" />.
        /// </typeparam>
        /// <exception cref="T:System.InvalidOperationException">
        /// The specified role returned an <see cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityCollection`1" /> instead of an
        /// <see
        ///     cref="T:System.Data.Entity.Core.Objects.DataClasses.EntityReference`1" />
        /// .
        /// </exception>
        public EntityReference<TTargetEntity> GetRelatedReference<TTargetEntity>(string relationshipName, string targetRoleName)
            where TTargetEntity : class
        {
            var reference =
                GetRelatedEndInternal(PrependNamespaceToRelationshipName(relationshipName), targetRoleName) as
                EntityReference<TTargetEntity>;
            if (reference == null)
            {
                throw new InvalidOperationException(
                    Strings.EntityReference_ExpectedReferenceGotCollection(typeof(TTargetEntity).Name, targetRoleName, relationshipName));
            }
            return reference;
        }

        // <summary>
        // Gets collection or ref of related entity for a particular navigation.
        // </summary>
        // <param name="navigation"> Describes the relationship and navigation direction </param>
        // <param name="relationshipFixer"> Encapsulates information about the other end's type and cardinality, and knows how to create the other end </param>
        internal RelatedEnd GetRelatedEnd(RelationshipNavigation navigation, IRelationshipFixer relationshipFixer)
        {
            RelatedEnd relatedEnd;

            if (TryGetCachedRelatedEnd(navigation.RelationshipName, navigation.To, out relatedEnd))
            {
                return relatedEnd;
            }
            else
            {
                relatedEnd = relationshipFixer.CreateSourceEnd(navigation, this);
                Debug.Assert(null != relatedEnd, "CreateSourceEnd should always return a valid RelatedEnd");

                return relatedEnd;
            }
        }

        // <summary>
        // Factory method for creating new related ends
        // </summary>
        // <typeparam name="TSourceEntity"> Type of the source end </typeparam>
        // <typeparam name="TTargetEntity"> Type of the target end </typeparam>
        // <param name="navigation"> RelationshipNavigation to be set on the new RelatedEnd </param>
        // <param name="sourceRoleMultiplicity"> Multiplicity of the source role </param>
        // <param name="targetRoleMultiplicity"> Multiplicity of the target role </param>
        // <param name="existingRelatedEnd"> An existing related end to initialize instead of creating a new one </param>
        // <returns> new EntityCollection or EntityReference, depending on the specified target multiplicity </returns>
        internal RelatedEnd CreateRelatedEnd<TSourceEntity, TTargetEntity>(
            RelationshipNavigation navigation, RelationshipMultiplicity sourceRoleMultiplicity,
            RelationshipMultiplicity targetRoleMultiplicity, RelatedEnd existingRelatedEnd)
            where TSourceEntity : class
            where TTargetEntity : class
        {
            IRelationshipFixer relationshipFixer = new RelationshipFixer<TSourceEntity, TTargetEntity>(
                sourceRoleMultiplicity, targetRoleMultiplicity);
            RelatedEnd relatedEnd = null;
            var wrappedOwner = WrappedOwner;
            switch (targetRoleMultiplicity)
            {
                case RelationshipMultiplicity.ZeroOrOne:
                case RelationshipMultiplicity.One:
                    if (existingRelatedEnd != null)
                    {
                        Debug.Assert(
                            wrappedOwner.Context == null || wrappedOwner.MergeOption == MergeOption.NoTracking,
                            "Expected null context when initializing an existing related end");
                        existingRelatedEnd.InitializeRelatedEnd(wrappedOwner, navigation, relationshipFixer);
                        relatedEnd = existingRelatedEnd;
                    }
                    else
                    {
                        relatedEnd = new EntityReference<TTargetEntity>(wrappedOwner, navigation, relationshipFixer);
                    }
                    break;
                case RelationshipMultiplicity.Many:
                    if (existingRelatedEnd != null)
                    {
                        Debug.Assert(
                            wrappedOwner.Context == null || wrappedOwner.MergeOption == MergeOption.NoTracking,
                            "Expected null context or NoTracking when initializing an existing related end");
                        existingRelatedEnd.InitializeRelatedEnd(wrappedOwner, navigation, relationshipFixer);
                        relatedEnd = existingRelatedEnd;
                    }
                    else
                    {
                        relatedEnd = new EntityCollection<TTargetEntity>(wrappedOwner, navigation, relationshipFixer);
                    }
                    break;
                default:
                    var type = typeof(RelationshipMultiplicity);
                    throw new ArgumentOutOfRangeException(
                        type.Name,
                        Strings.ADP_InvalidEnumerationValue(type.Name, ((int)targetRoleMultiplicity).ToString(CultureInfo.InvariantCulture)));
            }

            // Verify that we can attach the context successfully before adding to our list of relationships
            if (wrappedOwner.Context != null)
            {
                relatedEnd.AttachContext(wrappedOwner.Context, wrappedOwner.MergeOption);
            }

            EnsureRelationshipsInitialized();
            _relationships.Add(relatedEnd);

            return relatedEnd;
        }

        /// <summary>Returns an enumeration of all the related ends managed by the relationship manager.</summary>
        /// <returns>
        /// An <see cref="T:System.Collections.Generic.IEnumerable`1" /> of objects that implement
        /// <see
        ///     cref="T:System.Data.Entity.Core.Objects.DataClasses.IRelatedEnd" />
        /// . An empty enumeration is returned when the relationships have not yet been populated.
        /// </returns>
        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public IEnumerable<IRelatedEnd> GetAllRelatedEnds()
        {
            var wrappedOwner = WrappedOwner;

            EntityType entityType;
            if (wrappedOwner.Context != null
                && wrappedOwner.Context.MetadataWorkspace != null
                && TryGetOwnerEntityType(out entityType))
            {
                // For attached scenario:
                // MEST: This returns RelatedEnds representing AssociationTypes which belongs to AssociationSets 
                // which have one end of EntitySet of wrappedOwner.Entity's EntitySet
                Debug.Assert(wrappedOwner.EntityKey != null, "null entityKey on a attached entity");
                var entitySet = wrappedOwner.Context.GetEntitySet(
                    wrappedOwner.EntityKey.EntitySetName, wrappedOwner.EntityKey.EntityContainerName);
                foreach (var endMember in GetAllTargetEnds(entityType, entitySet))
                {
                    yield return GetRelatedEnd(endMember.DeclaringType.FullName, endMember.Name);
                }
            }
            else
            {
                // Disconnected scenario
                // MEST: this returns RelatedEnds representing all AssociationTypes which have one end of type of wrappedOwner.Entity's type.
                // The returned collection of RelatedEnds is a superset of RelatedEnds which can make sense for a single entity, because
                // an entity can belong only to one EntitySet.  Note that the ideal would be to return the same collection as for attached scenario,
                // but it's not possible because we don't know to which EntitySet the wrappedOwner.Entity belongs.
                if (wrappedOwner.Entity != null)
                {
                    foreach (var endMember in GetAllTargetEnds(wrappedOwner.IdentityType))
                    {
                        yield return GetRelatedEnd(endMember.DeclaringType.FullName, endMember.Name);
                    }
                }
            }
            yield break;
        }

        /// <summary>
        /// Called by Object Services to prepare an <see cref="T:System.Data.Entity.Core.EntityKey" /> for binary serialization with a serialized relationship.
        /// </summary>
        /// <param name="context">Describes the source and destination of a given serialized stream, and provides an additional caller-defined context.</param>
        [EditorBrowsable(EditorBrowsableState.Never)]
        [Browsable(false)]
        [OnSerializing]
        [SuppressMessage("Microsoft.Usage", "CA2238:ImplementSerializationMethodsCorrectly")]
        public void OnSerializing(StreamingContext context)
        {
            var wrappedOwner = WrappedOwner;
            if (!(wrappedOwner.Entity is IEntityWithRelationships))
            {
                throw new InvalidOperationException(Strings.RelatedEnd_CannotSerialize("RelationshipManager"));
            }
            // If we are attached to a context we need to go fixup the detached entity key on any EntityReferences
            if (wrappedOwner.Context != null
                && wrappedOwner.MergeOption != MergeOption.NoTracking)
            {
                foreach (RelatedEnd relatedEnd in GetAllRelatedEnds())
                {
                    var reference = relatedEnd as EntityReference;
                    if (reference != null
                        && reference.EntityKey != null)
                    {
                        reference.DetachedEntityKey = reference.EntityKey;
                    }
                }
            }
        }

        // ----------------
        // Internal Methods
        // ----------------

        internal bool HasRelationships
        {
            get { return _relationships != null; }
        }

        // <summary>
        // Add the rest of the graph, attached to this owner, to ObjectStateManager
        // </summary>
        // <param name="doAttach"> if TRUE, the rest of the graph is attached directly as Unchanged without calling AcceptChanges() </param>
        internal void AddRelatedEntitiesToObjectStateManager(bool doAttach)
        {
            if (null != _relationships)
            {
                var doCleanup = true;
                try
                {
                    // Create a copy of this list because with self references, the set of relationships can change
                    foreach (var relatedEnd in Relationships)
                    {
                        relatedEnd.Include( /*addRelationshipAsUnchanged*/false, doAttach);
                    }
                    doCleanup = false;
                }
                finally
                {
                    // If error happens, while attaching entity graph to context, clean-up
                    // is done on the Owner entity and all its relating entities.
                    if (doCleanup)
                    {
                        var wrappedOwner = WrappedOwner;
                        Debug.Assert(
                            wrappedOwner.Context != null && wrappedOwner.Context.ObjectStateManager != null,
                            "Null context or ObjectStateManager");

                        var transManager = wrappedOwner.Context.ObjectStateManager.TransactionManager;

                        // The graph being attached is connected to graph already existing in the OSM only through "promoted" relationships
                        // (relationships which originally existed only in OSM between key entries and entity entries but later were
                        // "promoted" to normal relationships in EntityRef/Collection when the key entries were promoted).
                        // The cleanup code traverse all the graph being added to the OSM, so we have to disconnect it from the graph already
                        // existing in the OSM by degrading promoted relationships.
                        wrappedOwner.Context.ObjectStateManager.DegradePromotedRelationships();

                        NodeVisited = true;
                        RemoveRelatedEntitiesFromObjectStateManager(wrappedOwner);

                        EntityEntry entry;

                        Debug.Assert(doAttach == (transManager.IsAttachTracking), "In attach the recovery collection should be not null");

                        if (transManager.IsAttachTracking
                            &&
                            transManager.PromotedKeyEntries.TryGetValue(wrappedOwner.Entity, out entry))
                        {
                            // This is executed only in the cleanup code from ObjectContext.AttachTo()
                            // If the entry was promoted in AttachTo(), it has to be degraded now instead of being deleted.
                            entry.DegradeEntry();
                        }
                        else
                        {
                            RelatedEnd.RemoveEntityFromObjectStateManager(wrappedOwner);
                        }
                    }
                }
            }
        }

        // Method is used to remove all entities and relationships, of a given entity
        // graph, from ObjectStateManager. This method is used when adding entity graph,
        // or a portion of it, raise exception. 
        internal static void RemoveRelatedEntitiesFromObjectStateManager(IEntityWrapper wrappedEntity)
        {
            DebugCheck.NotNull(wrappedEntity);
            foreach (var relatedEnd in wrappedEntity.RelationshipManager.Relationships)
            {
                // only some of the related ends may have gotten attached, so just skip the ones that weren't
                if (relatedEnd.ObjectContext != null)
                {
                    Debug.Assert(
                        !relatedEnd.UsingNoTracking,
                        "Shouldn't be touching the state manager with entities that were retrieved with NoTracking");
                    relatedEnd.Exclude();
                    relatedEnd.DetachContext();
                }
            }
        }

        // Remove entity from its relationships and do cascade delete if required.
        // All removed relationships are marked for deletion and all cascade deleted 
        // entitites are also marked for deletion.
        internal void RemoveEntityFromRelationships()
        {
            if (null != _relationships)
            {
                foreach (var relatedEnd in Relationships)
                {
                    relatedEnd.RemoveAll();
                }
            }
        }

        // <summary>
        // Traverse the relationships and find all the dependent ends that contain FKs, then attempt
        // to null all of those FKs.
        // </summary>
        [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Only cast twice in debug mode.")]
        internal void NullAllFKsInDependentsForWhichThisIsThePrincipal()
        {
            if (_relationships != null)
            {
                // Build a list of the dependent RelatedEnds because with overlapping FKs we could
                // end up removing a relationship before we have suceeded in nulling all the FK values
                // for that relationship.
                var dependentEndsToProcess = new List<EntityReference>();
                foreach (var relatedEnd in Relationships)
                {
                    if (relatedEnd.IsForeignKey)
                    {
                        foreach (var dependent in relatedEnd.GetWrappedEntities())
                        {
                            var dependentEnd = relatedEnd.GetOtherEndOfRelationship(dependent);
                            if (dependentEnd.IsDependentEndOfReferentialConstraint(checkIdentifying: false))
                            {
                                Debug.Assert(
                                    dependentEnd is EntityReference, "Dependent end in FK relationship should always be a reference.");
                                dependentEndsToProcess.Add((EntityReference)dependentEnd);
                            }
                        }
                    }
                }
                foreach (var dependentEnd in dependentEndsToProcess)
                {
                    dependentEnd.NullAllForeignKeys();
                }
            }
        }

        // Removes entity from its relationships.
        // Relationship entries are removed from ObjectStateManager if owner is in Added state 
        // or when owner is "many" end of the relationship
        internal void DetachEntityFromRelationships(EntityState ownerEntityState)
        {
            if (null != _relationships)
            {
                foreach (var relatedEnd in Relationships)
                {
                    relatedEnd.DetachAll(ownerEntityState);
                }
            }
        }

        //For a given relationship removes passed in entity from owners relationship
        internal void RemoveEntity(string toRole, string relationshipName, IEntityWrapper wrappedEntity)
        {
            DebugCheck.NotNull(wrappedEntity);
            RelatedEnd relatedEnd;
            if (TryGetCachedRelatedEnd(relationshipName, toRole, out relatedEnd))
            {
                relatedEnd.Remove(wrappedEntity, false);
            }
        }

        internal void ClearRelatedEndWrappers()
        {
            if (_relationships != null)
            {
                foreach (IRelatedEnd relatedEnd in Relationships)
                {
                    ((RelatedEnd)relatedEnd).ClearWrappedValues();
                }
            }
        }

        // Method used to retrieve properties from principal entities.
        // Parameter includeOwnValues means that values from current entity should be also added to "properties"
        // includeOwnValues is false only when this method is called from ObjectStateEntry.AcceptChanges()
        // Parmeter "visited" is a set containig entities which were already visited during traversing the graph.
        // If _owner already exists in the set, it means that there is a cycle in the graph of relationships with RI Constraints.
        internal void RetrieveReferentialConstraintProperties(
            out Dictionary<string, KeyValuePair<object, IntBox>> properties, HashSet<object> visited, bool includeOwnValues)
        {
            var wrappedOwner = WrappedOwner;
            Debug.Assert(wrappedOwner.Entity != null);
            DebugCheck.NotNull(visited);

            // Dictionary< propertyName, <propertyValue, counter>>
            properties = new Dictionary<string, KeyValuePair<object, IntBox>>();

            var ownerKey = wrappedOwner.EntityKey;
            Debug.Assert((object)ownerKey != null);

            // If the key is temporary, get values of referential constraint properties from principal entities
            if (ownerKey.IsTemporary)
            {
                // Find property names which should be retrieved
                List<string> propertiesToRetrieve;
                bool propertiesToPropagateExist; // not used

                FindNamesOfReferentialConstraintProperties(out propertiesToRetrieve, out propertiesToPropagateExist, skipFK: false);

                if (propertiesToRetrieve != null)
                {
                    // At first try to retrieve properties from entities which are in collections or references.
                    // This is the most common scenario.
                    // Only if properties couldn't be retrieved this way, try to retrieve properties from related stubs.

                    if (_relationships != null)
                    {
                        // Not using defensive copy here since RetrieveReferentialConstraintProperties should not cause change in underlying
                        // _relationships collection.
                        foreach (var relatedEnd in _relationships)
                        {
                            // NOTE: If the following call throws UnableToRetrieveReferentialConstraintProperties,
                            //       it means that properties couldn't be found in indirectly related entities,
                            //       so it doesn't make sense to search for properties in directly related stubs,
                            //       so exception is not being caught here.
                            relatedEnd.RetrieveReferentialConstraintProperties(properties, visited);
                        }
                    }

                    // Check if all properties were retrieved.
                    // There are 3 scenarios in which not every expected property can be retrieved:
                    // 1. There is no related entity from which the property is supposed to be retrieved.
                    // 2. Related entity which supposed to contains the property doesn't have fixed entity key.
                    // 3. Property should be retrieved from related key entry

                    if (!CheckIfAllPropertiesWereRetrieved(properties, propertiesToRetrieve))
                    {
                        // Properties couldn't be found in entities in collections or refrences.
                        // Try to find missing properties in related key entries.
                        // This process is slow but it is not a common case.
                        var entry = wrappedOwner.Context.ObjectStateManager.FindEntityEntry(ownerKey);
                        Debug.Assert(entry != null, "Owner entry not found in the object state manager");
                        entry.RetrieveReferentialConstraintPropertiesFromKeyEntries(properties);

                        // Check again if all properties were retrieved.
                        if (!CheckIfAllPropertiesWereRetrieved(properties, propertiesToRetrieve))
                        {
                            throw new InvalidOperationException(Strings.RelationshipManager_UnableToRetrieveReferentialConstraintProperties);
                        }
                    }
                }
            }

            // 1. If key is temporary, properties from principal entities were retrieved above. 
            //    The other key properties are properties which are not Dependent end of some Referential Constraint.
            // 2. If key is not temporary and this method was not called from AcceptChanges() - all key values
            //    of the current entity are added to 'properties'.
            if (!ownerKey.IsTemporary || includeOwnValues)
            {
                // NOTE this part is never executed when the method is called from ObjectStateManager.AcceptChanges(),
                //      so we don't try to "retrieve" properties from the the same (callers) entity.
                var entry = wrappedOwner.Context.ObjectStateManager.FindEntityEntry(ownerKey);
                Debug.Assert(entry != null, "Owner entry not found in the object state manager");
                entry.GetOtherKeyProperties(properties);
            }
        }

        // properties dictionary contains name of property, its value and coutner saying how many times this property was retrieved from principal entities
        private static bool CheckIfAllPropertiesWereRetrieved(
            Dictionary<string, KeyValuePair<object, IntBox>> properties, List<string> propertiesToRetrieve)
        {
            DebugCheck.NotNull(properties);
            DebugCheck.NotNull(propertiesToRetrieve);

            var isSuccess = true;

            var countersCopy = new List<int>();
            ICollection<KeyValuePair<object, IntBox>> values = properties.Values;

            // Create copy of counters (needed in case of failure)
            foreach (var valueCounterPair in values)
            {
                countersCopy.Add(valueCounterPair.Value.Value);
            }

            foreach (var name in propertiesToRetrieve)
            {
                if (!properties.ContainsKey(name))
                {
                    isSuccess = false;
                    break;
                }

                var valueCounterPair = properties[name];
                valueCounterPair.Value.Value = valueCounterPair.Value.Value - 1;
                if (valueCounterPair.Value.Value < 0)
                {
                    isSuccess = false;
                    break;
                }
            }

            // Check if all the coutners equal 0
            if (isSuccess)
            {
                foreach (var valueCounterPair in values)
                {
                    if (valueCounterPair.Value.Value != 0)
                    {
                        isSuccess = false;
                        break;
                    }
                }
            }

            // Restore counters in case of failure
            if (!isSuccess)
            {
                IEnumerator<int> enumerator = countersCopy.GetEnumerator();
                foreach (var valueCounterPair in values)
                {
                    enumerator.MoveNext();
                    valueCounterPair.Value.Value = enumerator.Current;
                }
            }

            return isSuccess;
        }

        // Check consistency between properties of current entity and Principal entities
        // If some of Principal entities don't exist or some property cannot be checked - this is violation of RI Constraints
        internal void CheckReferentialConstraintProperties(EntityEntry ownerEntry)
        {
            DebugCheck.NotNull(ownerEntry);
            if (HasReferentialConstraintPropertiesToCheck()
                && _relationships != null)
            {
                // Not using defensive copy here since CheckReferentialConstraintProperties should not cause change in underlying
                // _relationships collection.
                foreach (var relatedEnd in _relationships)
                {
                    relatedEnd.CheckReferentialConstraintProperties(ownerEntry);
                }
            }
        }

        // ----------------
        // Private Methods
        // ----------------

        // This method is required to maintain compatibility with the v1 binary serialization format. 
        // In particular, it recreates a entity wrapper from the serialized owner.
        // Note that this is only expected to work for non-POCO entities, since serialization of POCO
        // entities will not result in serialization of the RelationshipManager or its related objects.
        /// <summary>
        /// Used internally to deserialize entity objects along with the
        /// <see
        ///     cref="T:System.Data.Entity.Core.Objects.DataClasses.RelationshipManager" />
        /// instances.
        /// </summary>
        /// <param name="context">The serialized stream.</param>
        [EditorBrowsable(EditorBrowsableState.Never)]
        [Browsable(false)]
        [OnDeserialized]
        [SuppressMessage("Microsoft.Usage", "CA2238:ImplementSerializationMethodsCorrectly")]
        public void OnDeserialized(StreamingContext context)
        {
            // Note that when deserializing, the context is always null since we never serialize
            // the context with the entity.
            _entityWrapperFactory = new EntityWrapperFactory();
            _expensiveLoader = new ExpensiveOSpaceLoader();
            _wrappedOwner = EntityWrapperFactory.WrapEntityUsingContext(_owner, null);
        }

        // <summary>
        // Searches the list of relationships for an entry with the specified relationship name and role names
        // </summary>
        // <param name="relationshipName"> CSpace-qualified name of the relationship </param>
        // <param name="targetRoleName"> name of the target role </param>
        // <param name="relatedEnd"> the RelatedEnd if found, otherwise null </param>
        // <returns> true if the entry found, false otherwise </returns>
        private bool TryGetCachedRelatedEnd(string relationshipName, string targetRoleName, out RelatedEnd relatedEnd)
        {
            relatedEnd = null;
            if (null != _relationships)
            {
                // Not using defensive copy here since loop should not cause change in underlying
                // _relationships collection.
                foreach (var end in _relationships)
                {
                    var relNav = end.RelationshipNavigation;
                    if (relNav.RelationshipName == relationshipName
                        && relNav.To == targetRoleName)
                    {
                        relatedEnd = end;
                        return true;
                    }
                }
            }
            return false;
        }

        // Find properties which are Dependent/Principal ends of some referential constraint
        // Returned lists are never null.
        // NOTE This method will be removed when bug 505935 is solved
        // Returns true if any FK relationships were skipped so that they can be checked again after fixup
        internal bool FindNamesOfReferentialConstraintProperties(
            out List<string> propertiesToRetrieve, out bool propertiesToPropagateExist, bool skipFK)
        {
            var wrappedOwner = WrappedOwner;
            Debug.Assert(wrappedOwner.Entity != null);
            var ownerKey = wrappedOwner.EntityKey;
            if ((object)ownerKey == null)
            {
                throw Error.EntityKey_UnexpectedNull();
            }

            propertiesToRetrieve = null;
            propertiesToPropagateExist = false;

            if (wrappedOwner.Context == null)
            {
                throw new InvalidOperationException(Strings.RelationshipManager_UnexpectedNullContext);
            }
            var entitySet = ownerKey.GetEntitySet(wrappedOwner.Context.MetadataWorkspace);
            Debug.Assert(entitySet != null, "Unable to find entity set");

            // Get association types in which current entity's type is one of the ends.
            var associations = entitySet.AssociationSets;

            var skippedFK = false;
            // Find key property names which are part of referential integrity constraints
            foreach (var association in associations)
            {
                // NOTE ReferentialConstraints collection currently can contain 0 or 1 element
                if (skipFK && association.ElementType.IsForeignKey)
                {
                    skippedFK = true;
                }
                else
                {
                    foreach (var constraint in association.ElementType.ReferentialConstraints)
                    {
                        if (constraint.ToRole.TypeUsage.EdmType
                            == entitySet.ElementType.GetReferenceType())
                        {
                            // lazy creation of the list
                            propertiesToRetrieve = propertiesToRetrieve ?? new List<string>();
                            foreach (var property in constraint.ToProperties)
                            {
                                propertiesToRetrieve.Add(property.Name);
                            }
                        }
                        // There are schemas, in which relationship has the same entitySet on both ends
                        // that is why following 'if' statement is not inside of 'else' of previous 'if' statement
                        if (constraint.FromRole.TypeUsage.EdmType
                            == entitySet.ElementType.GetReferenceType())
                        {
                            propertiesToPropagateExist = true;
                        }
                    }
                }
            }
            return skippedFK;
        }

        // <summary>
        // Replaces FindNamesOfReferentialConstraintProperties where it was used to simply check if referential constraint properties 
        // exist while the list of properties created by FindNamesOfReferentialConstraintProperties was discarded. 
        // This method returns true or false indicating whether CheckReferentialConstraintProperties should be called for all relationships
        // </summary>
        internal bool HasReferentialConstraintPropertiesToCheck()
        {
            var wrappedOwner = WrappedOwner;
            Debug.Assert(wrappedOwner.Entity != null);
            var ownerKey = wrappedOwner.EntityKey;
            if ((object)ownerKey == null)
            {
                throw Error.EntityKey_UnexpectedNull();
            }

            if (wrappedOwner.Context == null)
            {
                throw new InvalidOperationException(Strings.RelationshipManager_UnexpectedNullContext);
            }
            var entitySet = ownerKey.GetEntitySet(wrappedOwner.Context.MetadataWorkspace);
            Debug.Assert(entitySet != null, "Unable to find entity set");

            // Get association types in which current entity's type is one of the ends.
            var associations = entitySet.AssociationSets;

            // Find key property names which are part of referential integrity constraints
            foreach (var association in associations)
            {
                    foreach (var constraint in association.ElementType.ReferentialConstraints)
                    {
                        if (constraint.ToRole.TypeUsage.EdmType
                            == entitySet.ElementType.GetReferenceType())
                        {
                            return true;
                        }
                        // There are schemas, in which relationship has the same entitySet on both ends
                        // that is why following 'if' statement is not inside of 'else' of previous 'if' statement
                        if (constraint.FromRole.TypeUsage.EdmType
                            == entitySet.ElementType.GetReferenceType())
                        {
                            return true;
                        }
                    }
            }
            return false;
        }

        // <summary>
        // Helper method to validate consistency of RelationshipManager instances
        // </summary>
        // <param name="wrappedEntity"> entity to compare against </param>
        // <returns> True if entity is the owner of this RelationshipManager, otherwise false </returns>
        internal bool IsOwner(IEntityWrapper wrappedEntity)
        {
            var wrappedOwner = WrappedOwner;
            Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null.");
            return ReferenceEquals(wrappedEntity.Entity, wrappedOwner.Entity);
        }

        // <summary>
        // Calls AttachContext on each RelatedEnd referenced by this manager.
        // </summary>
        internal void AttachContextToRelatedEnds(ObjectContext context, EntitySet entitySet, MergeOption mergeOption)
        {
            DebugCheck.NotNull(context);
            DebugCheck.NotNull(entitySet);
            if (null != _relationships)
            {
                // If GetAllRelatedEnds was called while the entity was not attached to the context
                // then _relationships may contain RelatedEnds that do not belong in based on the
                // entity set that the owner ultimately was attached to.  This means that when attaching
                // we need to trim the list to get rid of those RelatedEnds.
                // It is possible that the RelatedEnds may have been obtained explicitly rather than through
                // GetAllRelatedEnds.  If this is the case, then we prune anyway unless the RelatedEnd actually
                // has something attached to it, in which case we try to attach the context which will cause
                // an exception to be thrown.  This is all a bit messy, but it's the best we could do given that
                // GetAllRelatedEnds was implemented in 3.5sp1 without taking MEST into account.
                // Note that the Relationships property makes a copy so we can modify the list while iterating
                foreach (var relatedEnd in Relationships)
                {
                    EdmType relationshipType;
                    RelationshipSet relationshipSet;
                    relatedEnd.FindRelationshipSet(context, entitySet, out relationshipType, out relationshipSet);
                    if (relationshipSet != null
                        || !relatedEnd.IsEmpty())
                    {
                        relatedEnd.AttachContext(context, entitySet, mergeOption);
                    }
                    else
                    {
                        _relationships.Remove(relatedEnd);
                    }
                }
            }
        }

        // <summary>
        // Calls AttachContext on each RelatedEnd referenced by this manager and also on all the enties
        // referenced by that related end.
        // </summary>
        internal void ResetContextOnRelatedEnds(ObjectContext context, EntitySet entitySet, MergeOption mergeOption)
        {
            DebugCheck.NotNull(context);
            DebugCheck.NotNull(entitySet);
            if (null != _relationships)
            {
                foreach (var relatedEnd in Relationships)
                {
                    relatedEnd.AttachContext(context, entitySet, mergeOption);
                    foreach (var wrappedEntity in relatedEnd.GetWrappedEntities())
                    {
                        wrappedEntity.ResetContext(context, relatedEnd.GetTargetEntitySetFromRelationshipSet(), mergeOption);
                    }
                }
            }
        }

        // <summary>
        // Calls DetachContext on each RelatedEnd referenced by this manager.
        // </summary>
        internal void DetachContextFromRelatedEnds()
        {
            if (null != _relationships)
            {
                // Not using defensive copy here since DetachContext should not cause change in underlying
                // _relationships collection.
                foreach (var relatedEnd in _relationships)
                {
                    relatedEnd.DetachContext();
                }
            }
        }

        // --------------------
        // Internal definitions
        // --------------------

        [Conditional("DEBUG")]
        internal void VerifyIsNotRelated()
        {
            if (_relationships != null)
            {
                foreach (var r in _relationships)
                {
                    if (!r.IsEmpty())
                    {
                        Debug.Assert(
                            false,
                            "Cannot change a state of a Deleted entity if the entity has other than deleted relationships with other entities.");
                    }
                }
            }
        }
    }
}
