//--------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner [....] // @backupOwner [....] //--------------------------------------------------------------------- namespace System.Data.Objects.DataClasses { using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data.Common.Utils; using System.Data.Mapping; using System.Data.Metadata.Edm; using System.Data.Objects.Internal; using System.Diagnostics; using System.Linq; using System.Runtime.Serialization; /// /// Container for the lazily created relationship navigation /// property objects (collections and refs). /// [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() { } // ------ // 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 _relationships; [NonSerialized] private bool _nodeVisited; [NonSerialized] private IEntityWrapper _wrappedOwner; // ---------- // Properties // ---------- /// /// 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. /// internal IEnumerable Relationships { get { EnsureRelationshipsInitialized(); return _relationships.ToArray(); } } /// /// Lazy initialization of the _relationships collection. /// private void EnsureRelationshipsInitialized() { if (null == _relationships) { _relationships = new List(); } } /// /// this flag is used to keep track of nodes which have /// been visited. Currently used for Exclude operation. /// internal bool NodeVisited { get { return _nodeVisited; } set { _nodeVisited = value; } } /// /// Provides access to the entity that owns this manager in its wrapped form. /// internal IEntityWrapper WrappedOwner { get { if (_wrappedOwner == null) { _wrappedOwner = EntityWrapperFactory.CreateNewWrapper(_owner, null); } return _wrappedOwner; } } // ------- // Methods // ------- /// /// Factory method to create a new RelationshipManager object. /// /// 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. /// /// Reference to the entity that is calling this method /// is null /// A new or existing RelationshipManager for the given entity public static RelationshipManager Create(IEntityWithRelationships owner) { EntityUtil.CheckArgumentNull(owner, "owner"); RelationshipManager rm = new RelationshipManager(); rm._owner = owner; return rm; } /// /// 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. /// /// The new RelationshipManager internal static RelationshipManager Create() { return new RelationshipManager(); } /// /// Replaces the existing wrapped owner with one that potentially contains more information, /// such as an entity key. Both must wrap the same entity. /// 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 && !Object.ReferenceEquals(expectedOwner, _owner)) { throw EntityUtil.InvalidRelationshipManagerOwner(); } if (null != _relationships) { // Not using defensive copy here since SetWrappedOwner should not cause change in underlying // _relationships collection. foreach (RelatedEnd relatedEnd in _relationships) { relatedEnd.SetWrappedOwner(wrappedOwner); } } } /// /// Get the collection of entities related to the current entity using the specified /// combination of relationship name, source role name, and target role name /// /// Type of the entity in the source role (same as the type of this) /// Type of the entity in the target role /// CSpace-qualified name of the relationship to navigate /// Name of the source role for the navigation. Indicates the direction of navigation across the relationship. /// Name of the target role for the navigation. Indicates the direction of navigation across the relationship. /// Name of the property on the source of the navigation. /// Name of the property on the target of the navigation. /// Multiplicity of the source role. RelationshipMultiplicity.OneToOne and RelationshipMultiplicity.Zero are both /// accepted for a reference end, and RelationshipMultiplicity.Many is accepted for a collection /// Collection of related entities of type TTargetEntity internal EntityCollection GetRelatedCollection(string relationshipName, string sourceRoleName, string targetRoleName, NavigationPropertyAccessor sourceAccessor, NavigationPropertyAccessor targetAccessor, RelationshipMultiplicity sourceRoleMultiplicity, RelatedEnd existingRelatedEnd) where TSourceEntity : class where TTargetEntity : class { EntityCollection collection; RelatedEnd relatedEnd; TryGetCachedRelatedEnd(relationshipName, targetRoleName, out relatedEnd); if (existingRelatedEnd == null) { if (relatedEnd != null) { collection = relatedEnd as EntityCollection; // 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 or GetRelatedReference Debug.Assert(collection != null, "should never receive anything but an EntityCollection here"); return collection; } else { RelationshipNavigation navigation = new RelationshipNavigation(relationshipName, sourceRoleName, targetRoleName, sourceAccessor, targetAccessor); return CreateRelatedEnd(navigation, sourceRoleMultiplicity, RelationshipMultiplicity.Many, existingRelatedEnd) as EntityCollection; } } else { // There is no need to supress 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); } RelationshipNavigation navigation = new RelationshipNavigation(relationshipName, sourceRoleName, targetRoleName, sourceAccessor, targetAccessor); collection = CreateRelatedEnd(navigation, sourceRoleMultiplicity, RelationshipMultiplicity.Many, existingRelatedEnd) as EntityCollection; if (collection != null) { bool doCleanup = true; try { RemergeCollections(relatedEnd as EntityCollection, 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; } } /// /// Re-merge items from collection so that relationship fixup is performed. /// Ensure that any items in previous collection are excluded from the re-merge /// /// /// The previous EntityCollection containing items that have already had fixup performed /// The new EntityCollection private void RemergeCollections(EntityCollection previousCollection, EntityCollection collection) where TTargetEntity : class { Debug.Assert(collection != null, "collection is null"); // 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 int relatedEntityCount = 0; // We will be modifing the collection's enumerator, so we need to make a copy of it List tempEntities = new List(collection.CountInternal); foreach (IEntityWrapper 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 (IEntityWrapper wrappedEntity in tempEntities) { bool 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 EntityUtil.CannotRemergeCollections(); } } /// /// Get the entity reference of a related entity using the specified /// combination of relationship name, source role name, and target role name /// /// CSpace-qualified name of the relationship to navigate /// Name of the source role for the navigation. Indicates the direction of navigation across the relationship. /// Name of the target role for the navigation. Indicates the direction of navigation across the relationship. /// Name of the property on the source of the navigation. /// Name of the property on the target of the navigation. /// Multiplicity of the source role. RelationshipMultiplicity.OneToOne and RelationshipMultiplicity.Zero are both /// accepted for a reference end, and RelationshipMultiplicity.Many is accepted for a collection /// Reference for related entity of type TTargetEntity internal EntityReference GetRelatedReference(string relationshipName, string sourceRoleName, string targetRoleName, NavigationPropertyAccessor sourceAccessor, NavigationPropertyAccessor targetAccessor, RelationshipMultiplicity sourceRoleMultiplicity, RelatedEnd existingRelatedEnd) where TSourceEntity : class where TTargetEntity : class { EntityReference entityRef; RelatedEnd relatedEnd; if (TryGetCachedRelatedEnd(relationshipName, targetRoleName, out relatedEnd)) { entityRef = relatedEnd as EntityReference; // 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 or GetRelatedReference Debug.Assert(entityRef != null, "should never receive anything but an EntityReference here"); return entityRef; } else { RelationshipNavigation navigation = new RelationshipNavigation(relationshipName, sourceRoleName, targetRoleName, sourceAccessor, targetAccessor); return CreateRelatedEnd(navigation, sourceRoleMultiplicity, RelationshipMultiplicity.One, existingRelatedEnd) as EntityReference; } } /// /// Internal version of GetRelatedEnd that works with the o-space navigation property /// name rather than the c-space relationship name and end name. /// /// the name of the property to lookup /// the related end for the given property internal RelatedEnd GetRelatedEnd(string navigationProperty, bool throwArgumentException = false) { IEntityWrapper 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"); EntityType entityType = wrappedOwner.Context.MetadataWorkspace.GetItem(wrappedOwner.IdentityType.FullName, DataSpace.OSpace); EdmMember member; if (!wrappedOwner.Context.Perspective.TryGetMember(entityType, navigationProperty, false, out member) || !(member is NavigationProperty)) { var message = System.Data.Entity.Strings.RelationshipManager_NavigationPropertyNotFound(navigationProperty); throw throwArgumentException ? (Exception)new ArgumentException(message) : (Exception)new InvalidOperationException(message); } NavigationProperty navProp = (NavigationProperty)member; return GetRelatedEndInternal(navProp.RelationshipType.FullName, navProp.ToEndMember.Name); } /// /// Returns either an EntityCollection or EntityReference of the correct type for the specified target role in a relationship /// This is intended to be used in scenarios where the user doesn't have full metadata, including the static type /// information for both ends of the relationship. This metadata is specified in the EdmRelationshipRoleAttribute /// on each entity type in the relationship, so the metadata system can retrieve it based on the supplied relationship /// name and target role name. /// /// Name of the relationship in which targetRoleName is defined. Can be CSpace-qualified or not. /// Target role to use to retrieve the other end of relationshipName /// IRelatedEnd representing the EntityCollection or EntityReference that was retrieved 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) { EntityUtil.CheckArgumentNull(relationshipName, "relationshipName"); EntityUtil.CheckArgumentNull(targetRoleName, "targetRoleName"); IEntityWrapper wrappedOwner = WrappedOwner; if (wrappedOwner.Context == null && wrappedOwner.RequiresRelationshipChangeTracking) { throw new InvalidOperationException(System.Data.Entity.Strings.RelationshipManager_CannotGetRelatEndForDetachedPocoEntity); } RelatedEnd relatedEnd = null; // Try to get the AssociationType from metadata. This will contain all of the ospace metadata for this relationship AssociationType associationType = null; if (!TryGetRelationshipType(wrappedOwner, wrappedOwner.IdentityType, relationshipName, out associationType)) { if (_relationships != null) { // Look for the RelatedEnd in the list that has already been retrieved relatedEnd = (from RelatedEnd end in _relationships where end.RelationshipName == relationshipName && end.TargetRoleName == targetRoleName select end).FirstOrDefault(); } if (relatedEnd == null && !EntityProxyFactory.TryGetAssociationTypeFromProxyInfo(wrappedOwner, relationshipName, targetRoleName, out associationType)) { // If the end still cannot be found, throw an exception throw UnableToGetMetadata(WrappedOwner, relationshipName); } } if (relatedEnd == null) { Debug.Assert(associationType != null, "associationType is null"); relatedEnd = GetRelatedEndInternal(relationshipName, targetRoleName, /*existingRelatedEnd*/ null, associationType); } return relatedEnd; } private RelatedEnd GetRelatedEndInternal(string relationshipName, string targetRoleName, RelatedEnd existingRelatedEnd, AssociationType relationship) { return GetRelatedEndInternal(relationshipName, targetRoleName, existingRelatedEnd, relationship, true); } private RelatedEnd GetRelatedEndInternal(string relationshipName, string targetRoleName, RelatedEnd existingRelatedEnd, AssociationType relationship, bool throwOnError) { Debug.Assert(relationshipName != null, "null relationshipNameFromUser"); Debug.Assert(targetRoleName != null, "null targetRoleName"); // existingRelatedEnd can be null if we are not trying to initialize an existing end Debug.Assert(relationship != null, "null relationshipType"); AssociationEndMember sourceEnd; AssociationEndMember targetEnd; Debug.Assert(relationship.AssociationEndMembers.Count == 2, "Only 2-way relationships are currently supported"); RelatedEnd result = null; // There can only be two ends because we don't support n-way relationships -- figure out which end is the target and which is the source // If we want to support n-way relationships in the future, we will need a different overload of GetRelatedEnd that takes the source role name as well targetEnd = relationship.AssociationEndMembers[1]; if (targetEnd.Identity != targetRoleName) { sourceEnd = targetEnd; targetEnd = relationship.AssociationEndMembers[0]; if (targetEnd.Identity != targetRoleName) { if (throwOnError) { throw EntityUtil.InvalidTargetRole(relationshipName, targetRoleName, "targetRoleName"); } else { return result; } } } else { sourceEnd = relationship.AssociationEndMembers[0]; } // Validate that the source type matches the type of the owner EntityType sourceEntityType = MetadataHelper.GetEntityTypeForEnd(sourceEnd); Debug.Assert(sourceEntityType.DataSpace == DataSpace.OSpace && sourceEntityType.ClrType != null, "sourceEntityType must contain an ospace type"); Type sourceType = sourceEntityType.ClrType; IEntityWrapper wrappedOwner = WrappedOwner; if (!(sourceType.IsAssignableFrom(wrappedOwner.IdentityType))) { if (throwOnError) { throw EntityUtil.OwnerIsNotSourceType(wrappedOwner.IdentityType.FullName, sourceType.FullName, sourceEnd.Name, relationshipName); } } else if (VerifyRelationship(relationship, sourceEnd.Name, throwOnError)) { // Call a dynamic method that will call either GetRelatedCollection or GetRelatedReference for this relationship result = LightweightCodeGenerator.GetRelatedEnd(this, sourceEnd, targetEnd, existingRelatedEnd); } return result; } /// /// 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. /// /// Type of the entity represented by targetRoleName /// /// /// [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] public void InitializeRelatedReference(string relationshipName, string targetRoleName, EntityReference entityReference) where TTargetEntity : class { EntityUtil.CheckArgumentNull(relationshipName, "relationshipName"); EntityUtil.CheckArgumentNull(targetRoleName, "targetRoleName"); EntityUtil.CheckArgumentNull(entityReference, "entityReference"); if (entityReference.WrappedOwner.Entity != null) { throw EntityUtil.ReferenceAlreadyInitialized(); } IEntityWrapper wrappedOwner = WrappedOwner; if (wrappedOwner.Context != null && wrappedOwner.MergeOption != MergeOption.NoTracking) { throw EntityUtil.RelationshipManagerAttached(); } // 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); AssociationType relationship = GetRelationshipType(wrappedOwner.IdentityType, 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); } EntityReference reference = GetRelatedEndInternal(relationshipName, targetRoleName, entityReference, relationship) as EntityReference; if (reference == null) { throw EntityUtil.ExpectedReferenceGotCollection(typeof(TTargetEntity).Name, targetRoleName, relationshipName); } } /// /// 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. /// /// Type of the entity represented by targetRoleName /// /// /// [Browsable(false)] [EditorBrowsable(EditorBrowsableState.Never)] public void InitializeRelatedCollection(string relationshipName, string targetRoleName, EntityCollection entityCollection) where TTargetEntity : class { EntityUtil.CheckArgumentNull(relationshipName, "relationshipName"); EntityUtil.CheckArgumentNull(targetRoleName, "targetRoleName"); EntityUtil.CheckArgumentNull(entityCollection, "entityCollection"); if (entityCollection.WrappedOwner.Entity != null) { throw EntityUtil.CollectionAlreadyInitialized(); } IEntityWrapper wrappedOwner = WrappedOwner; if (wrappedOwner.Context != null && wrappedOwner.MergeOption != MergeOption.NoTracking) { throw EntityUtil.CollectionRelationshipManagerAttached(); } // 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); AssociationType relationship = GetRelationshipType(wrappedOwner.IdentityType, relationshipName); EntityCollection collection = GetRelatedEndInternal(relationshipName, targetRoleName, entityCollection, relationship) as EntityCollection; if (collection == null) { throw EntityUtil.ExpectedCollectionGotReference(typeof(TTargetEntity).Name, targetRoleName, relationshipName); } } /// /// 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. /// private string PrependNamespaceToRelationshipName(string relationshipName) { EntityUtil.CheckArgumentNull(relationshipName, "relationshipName"); if (!relationshipName.Contains('.')) { string identityName = WrappedOwner.IdentityType.FullName; ObjectItemCollection objectItemCollection = GetObjectItemCollection(WrappedOwner); EdmType entityType = null; if (objectItemCollection != null) { objectItemCollection.TryGetItem(identityName, out entityType); } else { Dictionary types = ObjectItemCollection.LoadTypesExpensiveWay(WrappedOwner.IdentityType.Assembly); if (types != null) { types.TryGetValue(identityName, out entityType); } } ClrEntityType clrEntityType = entityType as ClrEntityType; if (clrEntityType != null) { string ns = clrEntityType.CSpaceNamespaceName; Debug.Assert(!string.IsNullOrEmpty(ns), "Expected non-empty namespace for type."); return ns + "." + relationshipName; } } return relationshipName; } /// /// Trys to get an ObjectItemCollection and returns null if it can;t be found. /// private static ObjectItemCollection GetObjectItemCollection(IEntityWrapper wrappedOwner) { if (wrappedOwner.Context != null && wrappedOwner.Context.MetadataWorkspace != null) { return (ObjectItemCollection)wrappedOwner.Context.MetadataWorkspace.GetItemCollection(DataSpace.OSpace); } return null; } /// /// Trys to get the EntityType metadata and returns false if it can't be found. /// private bool TryGetOwnerEntityType(out EntityType entityType) { DefaultObjectMappingItemCollection mappings; Map map; if (TryGetObjectMappingItemCollection(WrappedOwner, out mappings) && mappings.TryGetMap(WrappedOwner.IdentityType.FullName, DataSpace.OSpace, out map)) { ObjectTypeMapping objectMap = (ObjectTypeMapping)map; if (Helper.IsEntityType(objectMap.EdmType)) { entityType = (EntityType)objectMap.EdmType; return true; } } entityType = null; return false; } /// /// Trys to get an DefaultObjectMappingItemCollection and returns false if it can't be found. /// 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 static bool TryGetRelationshipType(IEntityWrapper wrappedOwner, Type entityClrType, string relationshipName, out AssociationType associationType) { ObjectItemCollection objectItemCollection = GetObjectItemCollection(wrappedOwner); if (objectItemCollection != null) { associationType = objectItemCollection.GetRelationshipType(entityClrType, relationshipName); } else { associationType = ObjectItemCollection.GetRelationshipTypeExpensiveWay(entityClrType, relationshipName); } return (associationType != null); } private AssociationType GetRelationshipType(Type entityClrType, string relationshipName) { AssociationType associationType = null; if (!TryGetRelationshipType(WrappedOwner, entityClrType, relationshipName, out associationType)) { throw UnableToGetMetadata(WrappedOwner, relationshipName); } return associationType; } internal static Exception UnableToGetMetadata(IEntityWrapper wrappedOwner, string relationshipName) { ArgumentException argException = EntityUtil.UnableToFindRelationshipTypeInMetadata(relationshipName, "relationshipName"); if (EntityProxyFactory.IsProxyType(wrappedOwner.Entity.GetType())) { return EntityUtil.ProxyMetadataIsUnavailable(wrappedOwner.IdentityType, argException); } else { return argException; } } private IEnumerable GetAllTargetEnds(EntityType ownerEntityType, EntitySet ownerEntitySet) { foreach (AssociationSet assocSet in MetadataHelper.GetAssociationsForEntitySet(ownerEntitySet)) { EntityType end2EntityType = ((AssociationType)assocSet.ElementType).AssociationEndMembers[1].GetEntityType(); if (end2EntityType.IsAssignableFrom(ownerEntityType)) { yield return ((AssociationType)assocSet.ElementType).AssociationEndMembers[0]; } // not "else" because of associations between the same entity sets EntityType end1EntityType = ((AssociationType)assocSet.ElementType).AssociationEndMembers[0].GetEntityType(); if (end1EntityType.IsAssignableFrom(ownerEntityType)) { yield return ((AssociationType)assocSet.ElementType).AssociationEndMembers[1]; } } } /// /// 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 /// /// A CLR type that is on the source role of the relationship /// The OSpace EntityType that represents this CLR type private IEnumerable GetAllTargetEnds(Type entityClrType) { ObjectItemCollection objectItemCollection = GetObjectItemCollection(WrappedOwner); IEnumerable associations = null; if (objectItemCollection != null) { // Metadata is available associations = objectItemCollection.GetItems(); } else { // No metadata is available, attempt to load the metadata on the fly to retrieve the AssociationTypes associations = ObjectItemCollection.GetAllRelationshipTypesExpensiveWay(entityClrType.Assembly); } foreach (AssociationType association in associations) { // Check both ends for the presence of the source CLR type RefType 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]; } } yield break; } private bool VerifyRelationship(AssociationType relationship, string sourceEndName, bool throwOnError) { IEntityWrapper wrappedOwner = WrappedOwner; if (wrappedOwner.Context == null) { return true;// if not added to cache, can not decide- for now } EntityKey ownerKey = null; ownerKey = wrappedOwner.EntityKey; if (null == (object)ownerKey) { return true; // if not added to cache, can not decide- for now } TypeUsage associationTypeUsage; AssociationSet association = null; bool isVerified = true; // 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)) { //Get the entity container first EntityContainer entityContainer = wrappedOwner.Context.MetadataWorkspace.GetEntityContainer( ownerKey.EntityContainerName, DataSpace.CSpace); EntitySet entitySet; // Get the association set from the entity container, given the association type it refers to, and the entity set // name that the source end refers to association = MetadataHelper.GetAssociationsForEntitySetAndAssociationType(entityContainer, ownerKey.EntitySetName, (AssociationType)associationTypeUsage.EdmType, sourceEndName, out entitySet); if (association == null) { if (throwOnError) { throw EntityUtil.NoRelationshipSetMatched(relationship.FullName); } else { isVerified = false; } } else { Debug.Assert(association.AssociationSetEnds[sourceEndName].EntitySet == entitySet, "AssociationSetEnd does have the matching EntitySet"); } } return isVerified; } /// /// Get the collection of a related entity using the specified /// combination of relationship name, and target role name. /// Only supports 2-way relationships. /// /// Name of the relationship in which targetRoleName is defined. Can be CSpace-qualified or not. /// Name of the target role for the navigation. Indicates the direction of navigation across the relationship. /// Collection of entities of type TTargetEntity public EntityCollection GetRelatedCollection(string relationshipName, string targetRoleName) where TTargetEntity : class { EntityCollection collection = GetRelatedEndInternal(PrependNamespaceToRelationshipName(relationshipName), targetRoleName) as EntityCollection; if (collection == null) { throw EntityUtil.ExpectedCollectionGotReference(typeof(TTargetEntity).Name, targetRoleName, relationshipName); } return collection; } /// /// Get the entity reference of a related entity using the specified /// combination of relationship name, and target role name. /// Only supports 2-way relationships. /// /// Name of the relationship in which targetRoleName is defined. Can be CSpace-qualified or not. /// Name of the target role for the navigation. Indicates the direction of navigation across the relationship. /// Reference for related entity of type TTargetEntity public EntityReference GetRelatedReference(string relationshipName, string targetRoleName) where TTargetEntity : class { EntityReference reference = GetRelatedEndInternal(PrependNamespaceToRelationshipName(relationshipName), targetRoleName) as EntityReference; if (reference == null) { throw EntityUtil.ExpectedReferenceGotCollection(typeof(TTargetEntity).Name, targetRoleName, relationshipName); } return reference; } /// /// Gets collection or ref of related entity for a particular navigation. /// /// /// Describes the relationship and navigation direction /// /// /// Encapsulates information about the other end's type and cardinality, /// and knows how to create the other end /// /// 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; } } /// /// Factory method for creating new related ends /// /// Type of the source end /// Type of the target end /// RelationshipNavigation to be set on the new RelatedEnd /// Multiplicity of the source role /// Multiplicity of the target role /// An existing related end to initialize instead of creating a new one /// new EntityCollection or EntityReference, depending on the specified target multiplicity internal RelatedEnd CreateRelatedEnd(RelationshipNavigation navigation, RelationshipMultiplicity sourceRoleMultiplicity, RelationshipMultiplicity targetRoleMultiplicity, RelatedEnd existingRelatedEnd) where TSourceEntity : class where TTargetEntity : class { IRelationshipFixer relationshipFixer = new RelationshipFixer(sourceRoleMultiplicity, targetRoleMultiplicity); RelatedEnd relatedEnd = null; IEntityWrapper 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(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(wrappedOwner, navigation, relationshipFixer); } break; default: throw EntityUtil.InvalidEnumerationValue(typeof(RelationshipMultiplicity), (int)targetRoleMultiplicity); } // 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; } /// /// Returns an enumeration of all the related ends. The enumeration /// will be empty if the relationships have not been populated. /// public IEnumerable GetAllRelatedEnds() { IEntityWrapper 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"); EntitySet entitySet = wrappedOwner.Context.GetEntitySet(wrappedOwner.EntityKey.EntitySetName, wrappedOwner.EntityKey.EntityContainerName); foreach (AssociationEndMember 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 (AssociationEndMember endMember in GetAllTargetEnds(wrappedOwner.IdentityType)) { yield return GetRelatedEnd(endMember.DeclaringType.FullName, endMember.Name); } } } yield break; } [EditorBrowsable(EditorBrowsableState.Never)] [Browsable(false)] [OnSerializingAttribute] public void OnSerializing(StreamingContext context) { IEntityWrapper wrappedOwner = WrappedOwner; if (!(wrappedOwner.Entity is IEntityWithRelationships)) { throw new InvalidOperationException(System.Data.Entity.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()) { EntityReference reference = relatedEnd as EntityReference; if (reference != null && reference.EntityKey != null) { reference.DetachedEntityKey = reference.EntityKey; } } } } // ---------------- // Internal Methods // ---------------- internal bool HasRelationships { get { return _relationships != null; } } /// /// Add the rest of the graph, attached to this owner, to ObjectStateManager /// /// if TRUE, the rest of the graph is attached directly as Unchanged /// without calling AcceptChanges() internal void AddRelatedEntitiesToObjectStateManager(bool doAttach) { if (null != _relationships) { bool doCleanup = true; try { // Create a copy of this list because with self references, the set of relationships can change foreach (RelatedEnd 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) { IEntityWrapper wrappedOwner = WrappedOwner; Debug.Assert(wrappedOwner.Context != null && wrappedOwner.Context.ObjectStateManager != null, "Null context or ObjectStateManager"); TransactionManager 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) { Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null."); foreach (RelatedEnd 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 (RelatedEnd relatedEnd in Relationships) { relatedEnd.RemoveAll(); } } } /// /// Traverse the relationships and find all the dependent ends that contain FKs, then attempt /// to null all of those FKs. /// 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(); foreach (RelatedEnd relatedEnd in Relationships) { if (relatedEnd.IsForeignKey) { foreach (IEntityWrapper 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 (EntityReference 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 (RelatedEnd 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) { Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null."); 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> properties, HashSet visited, bool includeOwnValues) { IEntityWrapper wrappedOwner = WrappedOwner; Debug.Assert(wrappedOwner.Entity != null); Debug.Assert(visited != null); // Dictionary< propertyName, > properties = new Dictionary>(); EntityKey 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 propertiesToRetrieve; bool propertiesToPropagateExist; // not used this.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 (RelatedEnd 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. EntityEntry 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 EntityUtil.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. EntityEntry 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> properties, List propertiesToRetrieve) { Debug.Assert(properties != null); Debug.Assert(propertiesToRetrieve != null); bool isSuccess = true; List countersCopy = new List(); ICollection> values = properties.Values; // Create copy of counters (needed in case of failure) foreach (KeyValuePair valueCounterPair in values) { countersCopy.Add(valueCounterPair.Value.Value); } foreach (string name in propertiesToRetrieve) { if (!properties.ContainsKey(name)) { isSuccess = false; break; } KeyValuePair 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 (KeyValuePair valueCounterPair in values) { if (valueCounterPair.Value.Value != 0) { isSuccess = false; break; } } } // Restore counters in case of failure if (!isSuccess) { IEnumerator enumerator = countersCopy.GetEnumerator(); foreach (KeyValuePair 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) { Debug.Assert(ownerEntry != null); List propertiesToRetrieve; // used to check if the owner is a dependent end of some RI Constraint bool propertiesToPropagateExist; // used to check if the owner is a principal end of some RI Constraint this.FindNamesOfReferentialConstraintProperties(out propertiesToRetrieve, out propertiesToPropagateExist, skipFK: false); if ((propertiesToRetrieve != null || propertiesToPropagateExist) && _relationships != null) { // Not using defensive copy here since CheckReferentialConstraintProperties should not cause change in underlying // _relationships collection. foreach (RelatedEnd relatedEnd in _relationships) { if (!relatedEnd.CheckReferentialConstraintProperties(ownerEntry)) { throw EntityUtil.InconsistentReferentialConstraintProperties(); } } } } // ---------------- // 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. [EditorBrowsable(EditorBrowsableState.Never)] [Browsable(false)] [OnDeserialized()] public void OnDeserialized(StreamingContext context) { // Note that when deserializing, the context is always null since we never serialize // the context with the entity. _wrappedOwner = EntityWrapperFactory.WrapEntityUsingContext(_owner, null); } /// /// Searches the list of relationships for an entry with the specified relationship name and role names /// /// CSpace-qualified name of the relationship /// name of the target role /// the RelatedEnd if found, otherwise null /// true if the entry found, false otherwise 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 (RelatedEnd end in _relationships) { RelationshipNavigation 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 propertiesToRetrieve, out bool propertiesToPropagateExist, bool skipFK) { IEntityWrapper wrappedOwner = WrappedOwner; Debug.Assert(wrappedOwner.Entity != null); EntityKey ownerKey = wrappedOwner.EntityKey; EntityUtil.CheckEntityKeyNull(ownerKey); propertiesToRetrieve = null; propertiesToPropagateExist = false; EntityUtil.CheckContextNull(wrappedOwner.Context); EntitySet 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. List associations = MetadataHelper.GetAssociationsForEntitySet(entitySet); bool skippedFK = false; // Find key property names which are part of referential integrity constraints foreach (AssociationSet association in associations) { // NOTE ReferentialConstraints collection currently can contain 0 or 1 element if (skipFK && association.ElementType.IsForeignKey) { skippedFK = true; } else { foreach (ReferentialConstraint constraint in association.ElementType.ReferentialConstraints) { if (constraint.ToRole.TypeUsage.EdmType == entitySet.ElementType.GetReferenceType()) { // lazy creation of the list propertiesToRetrieve = propertiesToRetrieve ?? new List(); foreach (EdmProperty 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; } /// /// Helper method to validate consistency of RelationshipManager instances /// /// entity to compare against /// True if entity is the owner of this RelationshipManager, otherwise false internal bool IsOwner(IEntityWrapper wrappedEntity) { IEntityWrapper wrappedOwner = WrappedOwner; Debug.Assert(wrappedEntity != null, "IEntityWrapper instance is null."); return Object.ReferenceEquals(wrappedEntity.Entity, wrappedOwner.Entity); } /// /// Calls AttachContext on each RelatedEnd referenced by this manager. /// internal void AttachContextToRelatedEnds(ObjectContext context, EntitySet entitySet, MergeOption mergeOption) { Debug.Assert(null != context, "context"); Debug.Assert(null != entitySet, "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 (RelatedEnd 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); } } } } /// /// Calls AttachContext on each RelatedEnd referenced by this manager and also on all the enties /// referenced by that related end. /// internal void ResetContextOnRelatedEnds(ObjectContext context, EntitySet entitySet, MergeOption mergeOption) { Debug.Assert(null != context, "context"); Debug.Assert(null != entitySet, "entitySet"); if (null != _relationships) { foreach (RelatedEnd relatedEnd in Relationships) { relatedEnd.AttachContext(context, entitySet, mergeOption); foreach (IEntityWrapper wrappedEntity in relatedEnd.GetWrappedEntities()) { wrappedEntity.ResetContext(context, relatedEnd.GetTargetEntitySetFromRelationshipSet(), mergeOption); } } } } /// /// Calls DetachContext on each RelatedEnd referenced by this manager. /// internal void DetachContextFromRelatedEnds() { if (null != _relationships) { // Not using defensive copy here since DetachContext should not cause change in underlying // _relationships collection. foreach (RelatedEnd relatedEnd in _relationships) { relatedEnd.DetachContext(); } } } // -------------------- // Internal definitions // -------------------- [Conditional("DEBUG")] internal void VerifyIsNotRelated() { if (this._relationships != null) { foreach (var r in this._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."); } } } } } }