//----------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// @owner [....]
// @backupOwner [....]
//---------------------------------------------------------------------
using System.Data.Common;
using System.Data.Common.Utils;
using System.Collections.Generic;
using System.Diagnostics;
using System.Data.Mapping;
using System.Data.Metadata.Edm;
using System.Reflection;
using System.Collections;
using System.Runtime.Serialization;
using System.ComponentModel;
namespace System.Data.Objects.DataClasses
{
///
/// 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(IEntityWithRelationships owner)
{
EntityUtil.CheckArgumentNull(owner, "owner");
_owner = owner;
}
// ------
// 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.
private readonly IEntityWithRelationships _owner;
private List _relationships;
[NonSerialized]
private ObjectContext _context;
[NonSerialized]
private MergeOption _mergeOption;
[NonSerialized]
private bool _nodeVisited;
// ----------
// Properties
// ----------
internal List Relationships
{
get
{
if (null == _relationships)
{
_relationships = new List();
}
return _relationships;
}
}
///
/// Returns the current ObjectContext for this class.
///
internal ObjectContext Context
{
get
{
return _context;
}
}
internal MergeOption MergeOption
{
get
{
return _mergeOption;
}
}
///
/// 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;
}
}
// -------
// 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
///
public static RelationshipManager Create(IEntityWithRelationships owner)
{
return new RelationshipManager(owner);
}
///
/// Get the collection of entities related to the current entity using the specified
/// combination of relationship name, source role name, and target role name
/// This is private because it should only be called by the public GetRelatedEnd method.
///
/// 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.
/// 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
private EntityCollection GetRelatedCollection(string relationshipName, string sourceRoleName, string targetRoleName,
RelationshipMultiplicity sourceRoleMultiplicity, RelatedEnd existingRelatedEnd)
where TSourceEntity : class, IEntityWithRelationships
where TTargetEntity : class, IEntityWithRelationships
{
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);
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)
{
_relationships.Remove(relatedEnd);
}
RelationshipNavigation navigation = new RelationshipNavigation(relationshipName, sourceRoleName, targetRoleName);
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)
{
_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, IEntityWithRelationships
{
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.Count);
foreach (object entity in collection)
{
tempEntities.Add((IEntityWithRelationships)entity);
}
// 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 (IEntityWithRelationships entity 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(entity))
{
relatedEntityCount++;
requiresMerge = false;
}
}
if (requiresMerge)
{
// Remove and re-add the item to the collections to force fixup
((IRelatedEnd)collection).Remove(entity);
((IRelatedEnd)collection).Add(entity);
}
}
// Ensure that all of the items in the previousCollection are already in the new collection
if (previousCollection != null && relatedEntityCount != previousCollection.Count)
{
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
/// This is private because it should only be called by the public GetRelatedEnd method.
///
/// 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.
/// 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
private EntityReference GetRelatedReference(string relationshipName, string sourceRoleName, string targetRoleName,
RelationshipMultiplicity sourceRoleMultiplicity, RelatedEnd existingRelatedEnd)
where TSourceEntity : class, IEntityWithRelationships
where TTargetEntity : class, IEntityWithRelationships
{
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);
return CreateRelatedEnd(navigation, sourceRoleMultiplicity, RelationshipMultiplicity.One, existingRelatedEnd) as EntityReference;
}
}
///
/// 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)
{
EntityUtil.CheckArgumentNull(relationshipName, "relationshipName");
EntityUtil.CheckArgumentNull(targetRoleName, "targetRoleName");
// Get the AssociationType from metadata. This will contain all of the ospace metadata for this relationship
string fullSearchName;
AssociationType relationship = GetRelationshipType(_owner.GetType(), relationshipName, out fullSearchName);
return GetRelatedEndInternal(relationshipName, targetRoleName, /*existingRelatedEnd*/ null, relationship);
}
internal bool TryGetRelatedEnd(string relationshipName, string targetRoleName, out IRelatedEnd relatedEnd)
{
EntityUtil.CheckArgumentNull(relationshipName, "relationshipName");
EntityUtil.CheckArgumentNull(targetRoleName, "targetRoleName");
relatedEnd = null;
// Get the AssociationType from metadata. This will contain all of the ospace metadata for this relationship
string fullSearchName;
AssociationType relationship;
if(TryGetRelationshipType(_owner.GetType(), relationshipName, out fullSearchName, out relationship))
{
relatedEnd = GetRelatedEndInternal(relationshipName, targetRoleName, /*existingRelatedEnd*/ null, relationship, false);
return relatedEnd != null;
}
return false;
}
private IRelatedEnd GetRelatedEndInternal(string relationshipName, string targetRoleName, RelatedEnd existingRelatedEnd, AssociationType relationship)
{
return GetRelatedEndInternal(relationshipName, targetRoleName, existingRelatedEnd, relationship, true);
}
private IRelatedEnd 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");
IRelatedEnd 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
if (relationship.AssociationEndMembers.TryGetValue(targetRoleName, false, out targetEnd))
{
sourceEnd = MetadataHelper.GetOtherAssociationEnd(targetEnd);
}
else
{
if (throwOnError)
{
throw EntityUtil.InvalidTargetRole(relationshipName, targetRoleName, "targetRoleName");
}
else
{
return result;
}
}
// 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;
if (!(sourceType.IsAssignableFrom(_owner.GetType())))
{
if (throwOnError)
{
throw EntityUtil.OwnerIsNotSourceType(_owner.GetType().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)]
public void InitializeRelatedReference(string relationshipName, string targetRoleName, EntityReference entityReference)
where TTargetEntity : class, IEntityWithRelationships
{
EntityUtil.CheckArgumentNull(relationshipName, "relationshipName");
EntityUtil.CheckArgumentNull(targetRoleName, "targetRoleName");
EntityUtil.CheckArgumentNull(entityReference, "entityReference");
if (entityReference.Owner != null)
{
throw EntityUtil.ReferenceAlreadyInitialized();
}
if (this.Context != null && _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
string fullRelationshipName;
AssociationType relationship = GetRelationshipType(_owner.GetType(), relationshipName, out fullRelationshipName);
RelatedEnd relatedEnd;
if (TryGetCachedRelatedEnd(fullRelationshipName, 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);
}
_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)]
public void InitializeRelatedCollection(string relationshipName, string targetRoleName, EntityCollection entityCollection)
where TTargetEntity : class, IEntityWithRelationships
{
EntityUtil.CheckArgumentNull(relationshipName, "relationshipName");
EntityUtil.CheckArgumentNull(targetRoleName, "targetRoleName");
EntityUtil.CheckArgumentNull(entityCollection, "entityCollection");
if (entityCollection.Owner != null)
{
throw EntityUtil.CollectionAlreadyInitialized();
}
if (this.Context != null && _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
string fullRelationshipName;
AssociationType relationship = GetRelationshipType(_owner.GetType(), relationshipName, out fullRelationshipName);
EntityCollection collection = GetRelatedEndInternal(relationshipName, targetRoleName, entityCollection, relationship) as EntityCollection;
if (collection == null)
{
throw EntityUtil.ExpectedCollectionGotReference(typeof(TTargetEntity).Name, targetRoleName, relationshipName);
}
}
private bool TryGetRelationshipType(Type entityClrType, string relationshipName, out string fullSearchName, out AssociationType associationType)
{
ObjectItemCollection objectItemCollection = null;
if (_context != null && _context.MetadataWorkspace != null)
{
objectItemCollection = (ObjectItemCollection)_context.MetadataWorkspace.GetItemCollection(DataSpace.OSpace);
}
if (objectItemCollection != null)
{
associationType = objectItemCollection.GetRelationshipType(entityClrType, relationshipName, out fullSearchName);
}
else
{
associationType = ObjectItemCollection.GetRelationshipTypeExpensiveWay(entityClrType, relationshipName, out fullSearchName);
}
return (associationType != null);
}
private AssociationType GetRelationshipType(Type entityClrType, string relationshipName, out string fullSearchName)
{
AssociationType associationType = null;
if (!TryGetRelationshipType(entityClrType, relationshipName, out fullSearchName, out associationType))
{
throw EntityUtil.UnableToFindRelationshipTypeInMetadata(fullSearchName, "relationshipName");
}
return associationType;
}
///
/// 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 = null;
if (_context != null && _context.MetadataWorkspace != null)
{
objectItemCollection = (ObjectItemCollection)_context.MetadataWorkspace.GetItemCollection(DataSpace.OSpace);
}
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)
{
if (_context == null)
{
return true;// if not added to cache, can not decide- for now
}
EntityKey ownerKey = null;
ownerKey = ObjectContext.FindEntityKey(_owner, _context);
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 (_context.Perspective.TryGetTypeByName(relationship.FullName, false/*ignoreCase*/, out associationTypeUsage))
{
//Get the entity container first
EntityContainer entityContainer = this.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)
{
string fullyQualifiedRelationshipName = relationship.FullName;
throw EntityUtil.NoRelationshipSetMatched(fullyQualifiedRelationshipName);
}
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, IEntityWithRelationships
{
EntityCollection collection = GetRelatedEnd(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, IEntityWithRelationships
{
EntityReference reference = GetRelatedEnd(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 IRelatedEnd 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, IEntityWithRelationships
where TTargetEntity : class, IEntityWithRelationships
{
IRelationshipFixer relationshipFixer = new RelationshipFixer(sourceRoleMultiplicity, targetRoleMultiplicity);
RelatedEnd relatedEnd = null;
switch (targetRoleMultiplicity)
{
case RelationshipMultiplicity.ZeroOrOne:
case RelationshipMultiplicity.One:
if (existingRelatedEnd != null)
{
Debug.Assert(_context == null || _mergeOption == MergeOption.NoTracking, "Expected null context when initializing an existing related end");
existingRelatedEnd.InitializeRelatedEnd(_owner, navigation, relationshipFixer);
relatedEnd = existingRelatedEnd;
}
else
{
relatedEnd = new EntityReference(_owner, navigation, relationshipFixer);
}
break;
case RelationshipMultiplicity.Many:
if (existingRelatedEnd != null)
{
Debug.Assert(_context == null || MergeOption == MergeOption.NoTracking, "Expected null context or NoTracking when initializing an existing related end");
existingRelatedEnd.InitializeRelatedEnd(_owner, navigation, relationshipFixer);
relatedEnd = existingRelatedEnd;
}
else
{
relatedEnd = new EntityCollection(_owner, 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 (_context != null)
{
relatedEnd.AttachContext(_context, _mergeOption);
}
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()
{
if(_owner != null)
{
foreach (AssociationEndMember endMember in GetAllTargetEnds(_owner.GetType()))
{
yield return GetRelatedEnd(endMember.DeclaringType.FullName, endMember.Name);
}
}
yield break;
}
[OnSerializingAttribute]
public void OnSerializing(StreamingContext context)
{
// If we are attached to a context we need to go fixup the detached entity key on any EntityReferences
if (_context != null && _mergeOption != MergeOption.NoTracking)
{
// For all of the Added or Unchanged relationships that this owner participates in, if the relationship is a reference
// set the detached entity key to the EntityKey of the target member in the relationship
EntityKey ownerKey = _context.ObjectStateManager.GetEntityKey(_owner);
foreach (ObjectStateEntry relationshipEntry in _context.ObjectStateManager.FindRelationshipsByKey(ownerKey))
{
if (relationshipEntry.State != EntityState.Deleted)
{
EntityKey targetKey = relationshipEntry.Wrapper.GetOtherEntityKey(ownerKey);
EntityKey detachedEntityKey = targetKey;
if (!RelatedEnd.IsValidEntityKeyType(targetKey))
{
detachedEntityKey = null;
}
AssociationEndMember targetMember = relationshipEntry.Wrapper.GetAssociationEndMember(targetKey);
if (targetMember.RelationshipMultiplicity == RelationshipMultiplicity.One ||
targetMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne)
{
EntityReference entityRef = GetRelatedEnd(targetMember.DeclaringType.FullName, targetMember.Name) as EntityReference;
Debug.Assert(entityRef != null, "Expected EntityReference since target end multiplicity is one");
entityRef.DetachedEntityKey = detachedEntityKey;
}
}
}
}
// else the detached entity key is already the correct value for detached entities
}
// ----------------
// Internal Methods
// ----------------
internal void AttachContext(ObjectContext context, EntitySet entitySet, MergeOption mergeOption)
{
Debug.Assert(null != entitySet, "entitySet should not be null");
Debug.Assert(null != context, "context");
Debug.Assert(MergeOption.NoTracking == mergeOption ||
MergeOption.AppendOnly == mergeOption,
"mergeOption");
_context = context;
_mergeOption = mergeOption;
if (null != _relationships)
{
foreach(RelatedEnd relatedEnd in _relationships)
{
relatedEnd.AttachContext(context, entitySet, mergeOption);
}
}
}
internal void ResetContext(ObjectContext context, EntitySet entitySet, MergeOption mergeOption)
{
Debug.Assert(null != entitySet, "entitySet should not be null");
Debug.Assert(null != context, "context");
Debug.Assert(MergeOption.NoTracking == mergeOption ||
MergeOption.AppendOnly == mergeOption,
"mergeOption");
if (!object.ReferenceEquals(_context, context))
{
_context = context;
_mergeOption = mergeOption;
if (null != _relationships)
{
foreach (RelatedEnd relatedEnd in _relationships)
{
relatedEnd.AttachContext(context, entitySet, mergeOption);
foreach (object entity in relatedEnd)
{
IEntityWithRelationships entityWithRelationships = entity as IEntityWithRelationships;
if (entityWithRelationships != null)
{
entityWithRelationships.RelationshipManager.ResetContext(context, relatedEnd.GetTargetEntitySetFromRelationshipSet(), mergeOption);
}
}
}
}
}
}
internal void DetachContext()
{
_context = null;
if (null != _relationships)
{
foreach(RelatedEnd relatedEnd in _relationships)
{
relatedEnd.DetachContext();
}
}
}
///
/// 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)
{
HashSet promotedEntityKeyRefs = new HashSet();
bool doCleanup = true;
try
{
// Create a copy of this list because with self references, the set of relationships can change
List relatedEnds = new List(_relationships);
foreach(RelatedEnd relatedEnd in relatedEnds)
{
relatedEnd.Include(/*addRelationshipAsUnchanged*/false, doAttach, promotedEntityKeyRefs);
}
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)
{
Debug.Assert(this.Context != null && this.Context.ObjectStateManager != null, "Null context or ObjectStateManager");
if (this.Context.ObjectStateManager.IsAttachTracking)
{
// 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.
this.Context.ObjectStateManager.DegradePromotedRelationships();
}
NodeVisited = true;
RemoveRelatedEntitiesFromObjectStateManager(_owner, promotedEntityKeyRefs);
Debug.Assert(promotedEntityKeyRefs.Count == 0, "Haven't cleaned up all of the promoted reference EntityKeys");
ObjectStateEntry entry;
Debug.Assert(doAttach == (this.Context.ObjectStateManager.IsAttachTracking), "In attach the recovery collection should be not null");
if (this.Context.ObjectStateManager.IsAttachTracking &&
this.Context.ObjectStateManager.PromotedKeyEntries.TryGetValue(_owner, 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(_owner);
}
}
}
}
}
// 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(IEntityWithRelationships entity, HashSet promotedEntityKeyRefs)
{
foreach (RelatedEnd relatedEnd in entity.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(promotedEntityKeyRefs);
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();
}
}
}
// 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(RelationshipNavigation navigation, IEntityWithRelationships entity)
{
RelatedEnd relatedEnd;
if (TryGetCachedRelatedEnd(navigation.RelationshipName, navigation.To, out relatedEnd))
{
((IRelatedEnd)relatedEnd).Remove(entity);
}
}
// 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, bool includeOwnValues, HashSet